unrhodecode/rhodecode/authentication/plugins/services/ldap_dao.py
russell@unturf.com dc3502fed0 Fix session.save() for cookie sessions, add vcsserver/rc_license stubs
- Guard all session.save() calls with hasattr checks for CookieSession
  compatibility (CookieSession auto-saves via response callbacks)
- Guard beaker-specific _set_cookie_expires and _update_cookie_out in
  login views
- Defer ldap scope_labels to property to avoid class-level AttributeError
  when python-ldap is not installed
- Add vcsserver stub package to allow imports without full vcsserver
- Add rc_license stub for CE edition (no license management)
2026-02-19 19:38:09 -05:00

225 lines
8.1 KiB
Python

import logging
import traceback
from dataclasses import dataclass
from typing import List, Optional
from rhodecode.model.validators import Missing
from rhodecode.lib.exceptions import LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
from rhodecode.authentication.base import AuthLdapBase
from rhodecode.lib.str_utils import safe_str
try:
import ldap
except ImportError:
# means that python-ldap is not installed, we use Missing object to mark
# ldap lib is Missing
ldap = Missing
class LdapDao(AuthLdapBase):
default_tls_cert_dir = "/etc/openldap/cacerts"
@staticmethod
def _scope_labels():
return {
ldap.SCOPE_BASE: "SCOPE_BASE",
ldap.SCOPE_ONELEVEL: "SCOPE_ONELEVEL",
ldap.SCOPE_SUBTREE: "SCOPE_SUBTREE",
}
@property
def scope_labels(self):
return self._scope_labels()
def __init__(
self,
server,
base_dn,
port=389,
bind_dn="",
bind_pass="",
tls_kind="PLAIN",
tls_reqcert="DEMAND",
tls_cert_file=None,
tls_cert_dir=None,
ldap_version=3,
search_scope="SUBTREE",
attr_login="uid",
ldap_filter="",
timeout=None,
):
if ldap == Missing:
raise LdapImportError("Missing or incompatible ldap library")
self.log = logging.getLogger(LdapDao.__name__)
self.debug = False
self.timeout = timeout or 60 * 5
self.ldap_version = ldap_version
self.ldap_server_type = "ldap"
self.TLS_KIND = tls_kind
if self.TLS_KIND == "LDAPS":
port = port or 636
self.ldap_server_type += "s"
OPT_X_TLS_DEMAND = 2
self.TLS_REQCERT = getattr(ldap, "OPT_X_TLS_%s" % tls_reqcert, OPT_X_TLS_DEMAND)
self.TLS_CERT_FILE = tls_cert_file or ""
self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
# split server into list
self.SERVER_ADDRESSES = self._get_server_list(server)
self.LDAP_SERVER_PORT = port
# USE FOR READ ONLY BIND TO LDAP SERVER
self.attr_login = attr_login
self.LDAP_BIND_DN = safe_str(bind_dn)
self.LDAP_BIND_PASS = safe_str(bind_pass)
self.SEARCH_SCOPE = getattr(ldap, "SCOPE_%s" % search_scope)
self.BASE_DN = safe_str(base_dn)
self.LDAP_FILTER = safe_str(ldap_filter)
def _get_ldap_conn(self):
if self.debug:
ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
if self.TLS_CERT_FILE and hasattr(ldap, "OPT_X_TLS_CACERTFILE"):
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
elif hasattr(ldap, "OPT_X_TLS_CACERTDIR"):
ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
if self.TLS_KIND != "PLAIN":
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
# init connection now
ldap_servers = self._build_servers(self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
self.log.debug("initializing LDAP connection to:%s", ldap_servers)
ldap_conn = ldap.initialize(ldap_servers)
ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
ldap_conn.timeout = self.timeout
if self.ldap_version == 2:
ldap_conn.protocol = ldap.VERSION2
else:
ldap_conn.protocol = ldap.VERSION3
if self.TLS_KIND == "START_TLS":
ldap_conn.start_tls_s()
if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
self.log.debug("Trying simple_bind with password and given login DN: %r", self.LDAP_BIND_DN)
ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
self.log.debug("simple_bind successful")
return ldap_conn
def fetch_all(self, ldap_filter: str = "(objectClass=*)", attributes: List[str] = None) -> Optional[List[dict]]:
ldap_conn = None
try:
if attributes is None:
attributes = ["*", "+"]
ldap_conn = self._get_ldap_conn()
self.log.debug("fetching users for DN: %s", self.BASE_DN)
ldap_objects = ldap_conn.search_ext_s(self.BASE_DN, self.SEARCH_SCOPE, ldap_filter, attrlist=attributes)
return [attrs for _, attrs in ldap_objects]
except Exception as e:
self.log.error("Error fetching users for DN: %s. Error: %s", self.BASE_DN, str(e))
return None
finally:
self._releease_connection(ldap_conn)
def _releease_connection(self, ldap_conn):
if ldap_conn:
self.log.debug("ldap: connection release")
try:
ldap_conn.unbind_s()
except Exception as e:
# for any reason this can raise exception we must catch it
# to not crush the server
self.log.warning("unbind_s failed, error: %s", str(e))
def _fetch_attrs_from_simple_bind(self, ldap_conn, dn, username, password):
scope = ldap.SCOPE_BASE
scope_label = self.scope_labels.get(scope)
ldap_filter = "(objectClass=*)"
try:
self.log.debug(
"Trying authenticated search bind with dn: %r SCOPE: %s (and filter: %s)", dn, scope_label, ldap_filter
)
ldap_conn.simple_bind_s(dn, safe_str(password))
response = ldap_conn.search_ext_s(dn, scope, ldap_filter, attrlist=["*", "+"])
if not response:
self.log.error("search bind returned empty results: %r", response)
return {}
else:
_dn, attrs = response[0]
return attrs
except ldap.INVALID_CREDENTIALS:
self.log.debug("LDAP rejected password for user '%s': %s, org_exc:", username, dn, exc_info=True)
def authenticate_ldap(self, username, password):
"""
Authenticate a user via LDAP and return his/her LDAP properties.
Raises AuthenticationError if the credentials are rejected, or
EnvironmentError if the LDAP server can't be reached.
:param username: username
:param password: password
"""
uid = self.get_uid(username, self.SERVER_ADDRESSES)
self.validate_password(username, password)
self.validate_username(username)
scope_label = self.scope_labels.get(self.SEARCH_SCOPE)
ldap_conn = None
try:
ldap_conn = self._get_ldap_conn()
filter_ = "(&{}({}={}))".format(self.LDAP_FILTER, self.attr_login, username)
self.log.debug("Authenticating %r filter %s and scope: %s", self.BASE_DN, filter_, scope_label)
ldap_objects = ldap_conn.search_ext_s(self.BASE_DN, self.SEARCH_SCOPE, filter_, attrlist=["*", "+"])
if not ldap_objects:
self.log.debug("No matching LDAP objects for authentication of UID:'%s' username:(%s)", uid, username)
raise ldap.NO_SUCH_OBJECT()
self.log.debug(
"Found %s matching ldap object[s], trying to authenticate on each one now...", len(ldap_objects)
)
for dn, _attrs in ldap_objects:
if dn is None:
continue
user_attrs = self._fetch_attrs_from_simple_bind(ldap_conn, dn, username, password)
if user_attrs:
self.log.debug("Got authenticated user attributes from DN:%s", dn)
break
else:
raise LdapPasswordError(f"Failed to authenticate user `{username}` with given password")
except ldap.NO_SUCH_OBJECT:
self.log.debug("LDAP says no such user '%s' (%s), org_exc:", uid, username, exc_info=True)
raise LdapUsernameError("Unable to find user")
except ldap.SERVER_DOWN:
org_exc = traceback.format_exc()
raise LdapConnectionError("LDAP can't access authentication server, org_exc:%s" % org_exc)
finally:
self._releease_connection(ldap_conn)
return dn, user_attrs