Merge pull request !2768 from rhodecode-enterprise-ce feature/RCCE-226LDAP-Active-Directory]---periodic-sync-feature,-sync-active/inactive-user-status
feature: initial implementation of LDAP/AD sync
This commit is contained in:
commit
51bb9e8d14
8 changed files with 433 additions and 213 deletions
|
|
@ -21,28 +21,22 @@ RhodeCode authentication plugin for LDAP
|
|||
"""
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
import colander
|
||||
|
||||
from rhodecode.authentication.plugins.services.ldap_dao import LdapDao
|
||||
from rhodecode.translation import _
|
||||
from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property
|
||||
from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin, hybrid_property
|
||||
from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase, TwoFactorAuthnPluginSettingsSchemaMixin
|
||||
from rhodecode.authentication.routes import AuthnPluginResourceBase
|
||||
from rhodecode.lib.colander_utils import strip_whitespace
|
||||
from rhodecode.lib.exceptions import LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
|
||||
from rhodecode.lib.exceptions import LdapUsernameError, LdapPasswordError, LdapImportError
|
||||
from rhodecode.lib.str_utils import safe_str
|
||||
from rhodecode.model.db import User
|
||||
from rhodecode.model.validators import Missing
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
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 LdapError(Exception):
|
||||
pass
|
||||
|
|
@ -61,188 +55,6 @@ class LdapAuthnResource(AuthnPluginResourceBase):
|
|||
pass
|
||||
|
||||
|
||||
class AuthLdap(AuthLdapBase):
|
||||
default_tls_cert_dir = "/etc/openldap/cacerts"
|
||||
|
||||
scope_labels = {
|
||||
ldap.SCOPE_BASE: "SCOPE_BASE",
|
||||
ldap.SCOPE_ONELEVEL: "SCOPE_ONELEVEL",
|
||||
ldap.SCOPE_SUBTREE: "SCOPE_SUBTREE",
|
||||
}
|
||||
|
||||
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.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)
|
||||
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:
|
||||
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)
|
||||
log.debug("simple_bind successful")
|
||||
return ldap_conn
|
||||
|
||||
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:
|
||||
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:
|
||||
log.error("search bind returned empty results: %r", response)
|
||||
return {}
|
||||
else:
|
||||
_dn, attrs = response[0]
|
||||
return attrs
|
||||
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
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)
|
||||
user_attrs = {}
|
||||
dn = ""
|
||||
|
||||
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)
|
||||
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:
|
||||
log.debug("No matching LDAP objects for authentication of UID:'%s' username:(%s)", uid, username)
|
||||
raise ldap.NO_SUCH_OBJECT()
|
||||
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
if ldap_conn:
|
||||
log.debug("ldap: connection release")
|
||||
try:
|
||||
ldap_conn.unbind_s()
|
||||
except Exception:
|
||||
# for any reason this can raise exception we must catch it
|
||||
# to not crush the server
|
||||
pass
|
||||
|
||||
return dn, user_attrs
|
||||
|
||||
|
||||
class LdapSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSettingsSchemaBase):
|
||||
tls_kind_choices = ["PLAIN", "LDAPS", "START_TLS"]
|
||||
tls_reqcert_choices = ["NEVER", "ALLOW", "TRY", "DEMAND", "HARD"]
|
||||
|
|
@ -338,7 +150,7 @@ class LdapSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSet
|
|||
)
|
||||
tls_cert_dir = colander.SchemaNode(
|
||||
colander.String(),
|
||||
default=AuthLdap.default_tls_cert_dir,
|
||||
default=LdapDao.default_tls_cert_dir,
|
||||
description=_(
|
||||
"This specifies the path of a directory that contains individual CA certificates in separate files."
|
||||
),
|
||||
|
|
@ -420,6 +232,38 @@ class LdapSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSet
|
|||
title=_("Last Name Attribute"),
|
||||
widget="string",
|
||||
)
|
||||
sync_active_directory_users = colander.SchemaNode(
|
||||
colander.Bool(),
|
||||
default=False,
|
||||
description=_(
|
||||
"A cron job that periodically retrieves all users from an LDAP-based Active Directory server and syncs "
|
||||
"them with the RhodeCode database.\n"
|
||||
"Note: This feature is specific to Active Directory. Enabling it for other types of LDAP servers will have no effect."
|
||||
),
|
||||
missing=False,
|
||||
preparer=strip_whitespace,
|
||||
title=_("Sync Active Directory Users"),
|
||||
widget="bool",
|
||||
)
|
||||
|
||||
|
||||
def get_ldap_args(settings: dict):
|
||||
return {
|
||||
"server": settings.get("host", ""),
|
||||
"base_dn": settings.get("base_dn", ""),
|
||||
"port": settings.get("port"),
|
||||
"bind_dn": settings.get("dn_user"),
|
||||
"bind_pass": settings.get("dn_pass"),
|
||||
"tls_kind": settings.get("tls_kind"),
|
||||
"tls_reqcert": settings.get("tls_reqcert"),
|
||||
"tls_cert_file": settings.get("tls_cert_file"),
|
||||
"tls_cert_dir": settings.get("tls_cert_dir"),
|
||||
"search_scope": settings.get("search_scope"),
|
||||
"attr_login": settings.get("attr_login"),
|
||||
"ldap_version": 3,
|
||||
"ldap_filter": settings.get("filter"),
|
||||
"timeout": settings.get("timeout"),
|
||||
}
|
||||
|
||||
|
||||
class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
|
||||
|
|
@ -509,30 +353,15 @@ class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
|
|||
log.debug("Empty username or password skipping...")
|
||||
return None
|
||||
|
||||
ldap_args = {
|
||||
"server": settings.get("host", ""),
|
||||
"base_dn": settings.get("base_dn", ""),
|
||||
"port": settings.get("port"),
|
||||
"bind_dn": settings.get("dn_user"),
|
||||
"bind_pass": settings.get("dn_pass"),
|
||||
"tls_kind": settings.get("tls_kind"),
|
||||
"tls_reqcert": settings.get("tls_reqcert"),
|
||||
"tls_cert_file": settings.get("tls_cert_file"),
|
||||
"tls_cert_dir": settings.get("tls_cert_dir"),
|
||||
"search_scope": settings.get("search_scope"),
|
||||
"attr_login": settings.get("attr_login"),
|
||||
"ldap_version": 3,
|
||||
"ldap_filter": settings.get("filter"),
|
||||
"timeout": settings.get("timeout"),
|
||||
}
|
||||
ldap_args = get_ldap_args(settings)
|
||||
|
||||
ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
|
||||
|
||||
log.debug("Checking for ldap authentication.")
|
||||
|
||||
try:
|
||||
auth_ldap = AuthLdap(**ldap_args)
|
||||
(user_dn, ldap_attrs) = auth_ldap.authenticate_ldap(username, password)
|
||||
ldap_dao = LdapDao(**ldap_args)
|
||||
(user_dn, ldap_attrs) = ldap_dao.authenticate_ldap(username, password)
|
||||
log.debug("Got ldap DN response %s", user_dn)
|
||||
|
||||
def get_ldap_attr(k) -> str:
|
||||
|
|
|
|||
0
rhodecode/authentication/plugins/services/__init__.py
Normal file
0
rhodecode/authentication/plugins/services/__init__.py
Normal file
219
rhodecode/authentication/plugins/services/ldap_dao.py
Normal file
219
rhodecode/authentication/plugins/services/ldap_dao.py
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
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"
|
||||
|
||||
scope_labels = {
|
||||
ldap.SCOPE_BASE: "SCOPE_BASE",
|
||||
ldap.SCOPE_ONELEVEL: "SCOPE_ONELEVEL",
|
||||
ldap.SCOPE_SUBTREE: "SCOPE_SUBTREE",
|
||||
}
|
||||
|
||||
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
|
||||
0
rhodecode/authentication/tests/services/__init__.py
Normal file
0
rhodecode/authentication/tests/services/__init__.py
Normal file
52
rhodecode/authentication/tests/services/test_ldap_dao.py
Normal file
52
rhodecode/authentication/tests/services/test_ldap_dao.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import ldap
|
||||
|
||||
from rhodecode.authentication.plugins.services.ldap_dao import LdapDao
|
||||
from rhodecode.lib.diff_match_patch import patch_obj
|
||||
|
||||
|
||||
@patch("rhodecode.authentication.plugins.services.ldap_dao.ldap")
|
||||
class TestLdapDao(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._server_list = "test_srv1,test_srv2,test_srv3"
|
||||
self._base_dn = "test_dn"
|
||||
self.ldap_dao = LdapDao(
|
||||
server=self._server_list,
|
||||
base_dn=self._base_dn,
|
||||
)
|
||||
|
||||
@patch.object(LdapDao, "_get_ldap_conn")
|
||||
def test_fetch_all_from_ldap_server(self, _get_ldap_conn_mock, ldap_mock):
|
||||
conn = MagicMock()
|
||||
no_objects_in_ldap_server = []
|
||||
conn.search_ext_s.return_value = no_objects_in_ldap_server
|
||||
|
||||
_get_ldap_conn_mock.return_value = conn
|
||||
|
||||
returned_value = self.ldap_dao.fetch_all()
|
||||
|
||||
conn.search_ext_s.assert_called_once_with(self._base_dn, 2, "(objectClass=*)", attrlist=["*", "+"])
|
||||
|
||||
assert returned_value == no_objects_in_ldap_server
|
||||
|
||||
@patch.object(LdapDao, "_get_ldap_conn")
|
||||
def test_fetch_all_release_connection(self, _get_ldap_conn_mock, ldap_mock):
|
||||
conn = MagicMock()
|
||||
_get_ldap_conn_mock.return_value = conn
|
||||
|
||||
self.ldap_dao.fetch_all()
|
||||
|
||||
conn.unbind_s.assert_called_once()
|
||||
|
||||
@patch.object(LdapDao, "_get_ldap_conn")
|
||||
def test_fetch_all_release_connection_on_error(self, _get_ldap_conn_mock, ldap_mock):
|
||||
conn = MagicMock()
|
||||
_get_ldap_conn_mock.return_value = conn
|
||||
conn.search_ext_s.side_effect = Exception("Test exception")
|
||||
|
||||
res = self.ldap_dao.fetch_all()
|
||||
|
||||
assert res is None
|
||||
conn.unbind_s.assert_called_once()
|
||||
63
rhodecode/authentication/tests/test_auth_plugin_view.py
Normal file
63
rhodecode/authentication/tests/test_auth_plugin_view.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from rhodecode.authentication.views import AuthnPluginViewBase
|
||||
from rhodecode.lib.celerylib import tasks
|
||||
|
||||
|
||||
class TestAuthPluginView:
|
||||
@pytest.mark.parametrize(
|
||||
"sync_active_directory_users, expected_celery_task",
|
||||
[
|
||||
(
|
||||
True,
|
||||
tasks.schedule_sync_ldap_ad_users_producer,
|
||||
),
|
||||
(
|
||||
False,
|
||||
tasks.unschedule_sync_ldap_ad_users_producer,
|
||||
),
|
||||
],
|
||||
)
|
||||
@patch("rhodecode.apps._base.BaseAppView.__init__", return_value=None)
|
||||
@patch("rhodecode.authentication.views.h")
|
||||
@patch("rhodecode.authentication.views.SettingsModel")
|
||||
@patch.object(AuthnPluginViewBase, "load_default_context")
|
||||
@patch("rhodecode.authentication.views.run_task")
|
||||
def test_enable_ad_sync_schedule(
|
||||
self,
|
||||
run_task_mock,
|
||||
load_default_context,
|
||||
settings_model,
|
||||
h,
|
||||
_init_,
|
||||
sync_active_directory_users,
|
||||
expected_celery_task,
|
||||
):
|
||||
view = self._get_instance()
|
||||
|
||||
settings_post = AuthnPluginViewBase.__dict__[ # ignore decorators
|
||||
"settings_post"
|
||||
].__wrapped__.__wrapped__.__wrapped__
|
||||
|
||||
schema = MagicMock()
|
||||
schema.deserialize.return_value = {"sync_active_directory_users": sync_active_directory_users}
|
||||
|
||||
self._plugin.get_settings_schema.return_value = schema
|
||||
|
||||
settings_post(view)
|
||||
|
||||
run_task_mock.assert_called_once_with(expected_celery_task)
|
||||
|
||||
def _get_instance(self):
|
||||
# since base __init__ was patched, there is a need to inject mocks manually
|
||||
self._context = MagicMock()
|
||||
self._request = MagicMock()
|
||||
self._plugin = MagicMock()
|
||||
|
||||
view = AuthnPluginViewBase(self._context, self._request)
|
||||
view.request = self._request
|
||||
view.plugin = self._plugin
|
||||
view.context = self._context
|
||||
return view
|
||||
|
|
@ -28,6 +28,7 @@ from rhodecode.apps._base import BaseAppView
|
|||
from rhodecode.authentication.base import get_authn_registry
|
||||
from rhodecode.lib import helpers as h
|
||||
from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, CSRFRequired
|
||||
from rhodecode.lib.celerylib import run_task, tasks
|
||||
from rhodecode.model.forms import AuthSettingsForm
|
||||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.settings import SettingsModel
|
||||
|
|
@ -90,6 +91,9 @@ class AuthnPluginViewBase(BaseAppView):
|
|||
# Store validated data.
|
||||
for name, value in valid_data.items():
|
||||
self.plugin.create_or_update_setting(name, value)
|
||||
if name == "sync_active_directory_users":
|
||||
self._handle_ad_users_sync_schedule(name, value)
|
||||
|
||||
Session().commit()
|
||||
SettingsModel().invalidate_settings_cache()
|
||||
|
||||
|
|
@ -102,6 +106,17 @@ class AuthnPluginViewBase(BaseAppView):
|
|||
|
||||
return HTTPFound(redirect_to)
|
||||
|
||||
def _handle_ad_users_sync_schedule(self, name: str, enabled: bool):
|
||||
if name != "sync_active_directory_users":
|
||||
return
|
||||
|
||||
if enabled:
|
||||
log.debug("Scheduling AD users sync")
|
||||
run_task(tasks.schedule_sync_ldap_ad_users_producer)
|
||||
else:
|
||||
log.debug("Removing schedule for AD users sync")
|
||||
run_task(tasks.unschedule_sync_ldap_ad_users_producer)
|
||||
|
||||
|
||||
class AuthSettingsView(BaseAppView):
|
||||
def load_default_context(self):
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ by celery daemon
|
|||
import os
|
||||
import time
|
||||
|
||||
from celery import current_app
|
||||
from pyramid_mailer.mailer import Mailer
|
||||
from pyramid_mailer.message import Message
|
||||
from email.utils import formatdate
|
||||
|
|
@ -35,7 +36,7 @@ from rhodecode.lib import hooks_base
|
|||
from rhodecode.lib.utils import adopt_for_celery
|
||||
from rhodecode.lib.utils2 import safe_int, str2bool, aslist
|
||||
from rhodecode.lib.statsd_client import StatsdClient
|
||||
from rhodecode.model.db import true, null, Session, IntegrityError, Repository, RepoGroup, User
|
||||
from rhodecode.model.db import true, null, Session, IntegrityError, Repository, RepoGroup, User, ScheduleEntry
|
||||
from rhodecode.model.permission import PermissionModel
|
||||
|
||||
|
||||
|
|
@ -417,6 +418,47 @@ def beat_check(*args, **kwargs):
|
|||
return time.time()
|
||||
|
||||
|
||||
@async_task(ignore_result=True)
|
||||
def schedule_sync_ldap_ad_users_producer():
|
||||
log = get_logger(schedule_sync_ldap_ad_users_producer)
|
||||
|
||||
try:
|
||||
from rc_ee.lib.celerylib.scheduler import RcScheduler
|
||||
except ImportError:
|
||||
log.error("Attempt to schedule EE feature")
|
||||
return
|
||||
|
||||
scheduler = RcScheduler(app=current_app)
|
||||
scheduler.sync()
|
||||
task_name = "rc_ee.lib.celerylib.tasks.sync_ldap_ad_users_producer"
|
||||
if task_name not in scheduler.schedule:
|
||||
entries = {
|
||||
task_name: {
|
||||
"task": task_name,
|
||||
"schedule_type": "crontab",
|
||||
"schedule_value": {"hour": 0, "minute": 4},
|
||||
"options": {"expires": 12 * 3600},
|
||||
}
|
||||
}
|
||||
scheduler.update_from_dict(entries)
|
||||
|
||||
|
||||
@async_task(ignore_result=True)
|
||||
def unschedule_sync_ldap_ad_users_producer():
|
||||
log = get_logger(unschedule_sync_ldap_ad_users_producer)
|
||||
|
||||
try:
|
||||
from rc_ee.lib.celerylib.scheduler import RcScheduler
|
||||
except ImportError:
|
||||
log.error("Attempt to unschedule EE feature")
|
||||
return
|
||||
|
||||
task_name = "rc_ee.lib.celerylib.tasks.sync_ldap_ad_users_producer"
|
||||
existing_task = ScheduleEntry.query().filter(ScheduleEntry.task_dot_notation == task_name).first()
|
||||
if existing_task:
|
||||
Session().delete(existing_task)
|
||||
|
||||
|
||||
@async_task
|
||||
@adopt_for_celery
|
||||
def repo_size(extras):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue