|
26 | 26 | from abc import ABC, abstractmethod
|
27 | 27 | from typing import Any, Tuple, Union
|
28 | 28 | from urllib.parse import urlencode
|
| 29 | +import os |
| 30 | +import ssl |
| 31 | +from typing import Union |
| 32 | +from ldap3 import Server, Connection, ALL, SUBTREE, Tls |
| 33 | +from ldap3.core.exceptions import LDAPException, LDAPAttributeError, LDAPBindError, LDAPSocketReceiveError |
29 | 34 |
|
30 | 35 | import requests
|
31 | 36 |
|
@@ -174,6 +179,7 @@ def form_html(self) -> str:
|
174 | 179 | def form_html(self, html: str):
|
175 | 180 | self._custom_form = html
|
176 | 181 |
|
| 182 | + |
177 | 183 | class BaseAuthenticator(ABC):
|
178 | 184 | """
|
179 | 185 | Base class for authenticators.
|
@@ -355,6 +361,163 @@ def get_auth_info(self) -> Tuple[str, str]:
|
355 | 361 | return oauth_state, provider_url
|
356 | 362 |
|
357 | 363 |
|
| 364 | +class CAEDMAuthenticator(BaseAuthenticator): |
| 365 | + def __init__(self, ca_cert_path: str = '/etc/ssl/certs', connection_timeout: int = 5): |
| 366 | + self.ca_cert_path = ca_cert_path |
| 367 | + self.connection_timeout = connection_timeout |
| 368 | + |
| 369 | + # LDAP Server Configuration |
| 370 | + self.ldap_addresses = [ |
| 371 | + 'ctldap.et.byu.edu', |
| 372 | + 'cbldap.et.byu.edu' |
| 373 | + ] |
| 374 | + self.ldap_base_dn = 'ou=accounts,ou=caedm,dc=et,dc=byu,dc=edu' |
| 375 | + |
| 376 | + # Ensure certificate directory exists |
| 377 | + if not os.path.exists(self.ca_cert_path): |
| 378 | + raise FileNotFoundError('Path to CA Certificates directory does not exist') |
| 379 | + |
| 380 | + # Initialize LDAP server instances |
| 381 | + self.ldap_server_instances = self._initialize_ldap_servers() |
| 382 | + self.ldap_usable_servers = self._test_ldap_anonymous_bind() |
| 383 | + self._next_server_index = 0 |
| 384 | + self._login_style = LoginStyle('c-square-fill', 'maeser.login', direct_submit=False) |
| 385 | + |
| 386 | + def __str__(self): |
| 387 | + return "CAEDM" |
| 388 | + |
| 389 | + @property |
| 390 | + def style(self): |
| 391 | + return self._login_style |
| 392 | + |
| 393 | + @property |
| 394 | + def next_ldap_server(self)-> Union[Server, None]: |
| 395 | + """Return the next available LDAP server in a round-robin fashion.""" |
| 396 | + if len(self.ldap_usable_servers) == 0: |
| 397 | + print("NO REACHABLE LDAP SERVER!") |
| 398 | + return None |
| 399 | + server_to_use = self.ldap_usable_servers[self._next_server_index] |
| 400 | + self._next_server_index = (self._next_server_index + 1) % len(self.ldap_usable_servers) |
| 401 | + return server_to_use |
| 402 | + |
| 403 | + def _initialize_ldap_servers(self) -> list[Server]: |
| 404 | + """ |
| 405 | + Initialize LDAP server instances with retrieved certificates. |
| 406 | +
|
| 407 | + Returns: |
| 408 | + list: A list of initialized LDAP Server objects. |
| 409 | + """ |
| 410 | + servers: list[Server] = [] |
| 411 | + for server_url in self.ldap_addresses: |
| 412 | + try: |
| 413 | + servers.append(Server( |
| 414 | + server_url, |
| 415 | + use_ssl=True, |
| 416 | + get_info=ALL, |
| 417 | + connect_timeout=self.connection_timeout, |
| 418 | + tls=Tls(validate=ssl.CERT_REQUIRED, ca_certs_path=self.ca_cert_path) |
| 419 | + )) |
| 420 | + except LDAPException as e: |
| 421 | + print(f'Unable to initialize LDAP server {server_url}: {type(e)}, {e}') |
| 422 | + return servers |
| 423 | + |
| 424 | + def _test_ldap_anonymous_bind(self) -> list: |
| 425 | + """ |
| 426 | + Test anonymous bind to each LDAP server and blacklist bad servers. |
| 427 | +
|
| 428 | + Returns: |
| 429 | + list: A list of LDAP servers that are usable. |
| 430 | + """ |
| 431 | + usable_servers = [] |
| 432 | + for ldap_server in self.ldap_server_instances: |
| 433 | + try: |
| 434 | + test_connection = Connection(ldap_server, receive_timeout=self.connection_timeout) |
| 435 | + if test_connection.bind(): |
| 436 | + usable_servers.append(ldap_server) |
| 437 | + test_connection.unbind() |
| 438 | + except (LDAPException, LDAPAttributeError, LDAPBindError, LDAPSocketReceiveError) as e: |
| 439 | + print(f'Failed to bind to LDAP server {ldap_server}: {type(e)}, {e}') |
| 440 | + return usable_servers |
| 441 | + |
| 442 | + def authenticate(self, ident: str, password: str) -> Union[tuple, None]: |
| 443 | + if self.next_ldap_server is None: |
| 444 | + return None |
| 445 | + try: |
| 446 | + conn = Connection( |
| 447 | + self.next_ldap_server, |
| 448 | + user=f'cn={ident},{self.ldap_base_dn}', |
| 449 | + password=password, |
| 450 | + auto_bind=True, |
| 451 | + read_only=True, |
| 452 | + receive_timeout=self.connection_timeout |
| 453 | + ) |
| 454 | + except (LDAPException, LDAPAttributeError, LDAPBindError, LDAPSocketReceiveError) as e: |
| 455 | + print(f'CAEDM user {ident} failed to authenticate: {type(e)}: {e}') |
| 456 | + return None |
| 457 | + |
| 458 | + try: |
| 459 | + conn.search( |
| 460 | + self.ldap_base_dn, |
| 461 | + f'(cn={ident})', |
| 462 | + SUBTREE, |
| 463 | + attributes=['cn', 'displayName', 'CAEDMUserType'], |
| 464 | + time_limit=int(self.connection_timeout) |
| 465 | + ) |
| 466 | + if conn.entries: |
| 467 | + display_name = conn.entries[0].displayName |
| 468 | + user_group = conn.entries[0].CAEDMUserType |
| 469 | + return ident, display_name, user_group |
| 470 | + except LDAPSocketReceiveError as e: |
| 471 | + print(f'LDAP search timed out for user {ident}: {e}') |
| 472 | + finally: |
| 473 | + conn.unbind() |
| 474 | + |
| 475 | + return None |
| 476 | + |
| 477 | + def fetch_user(self, ident: str) -> Union[User, None]: |
| 478 | + """ |
| 479 | + Fetch user information from LDAP and return a User object. |
| 480 | + This method performs an anonymous bind and searches for the user. |
| 481 | + No authentication is preformed using this method. |
| 482 | +
|
| 483 | + Args: |
| 484 | + ident (str): The user's identifier. |
| 485 | +
|
| 486 | + Returns: |
| 487 | + Union[User, None]: The User object if found, None otherwise. |
| 488 | + """ |
| 489 | + if self.next_ldap_server is None: |
| 490 | + return None |
| 491 | + try: |
| 492 | + conn = Connection(self.next_ldap_server, auto_bind=True, receive_timeout=self.connection_timeout) |
| 493 | + except (LDAPException, LDAPAttributeError, LDAPBindError, LDAPSocketReceiveError) as e: |
| 494 | + print(f'LDAP fetch for user {ident} failed: {type(e)}, {e}') |
| 495 | + return None |
| 496 | + |
| 497 | + try: |
| 498 | + search_filter = f'(cn={ident})' |
| 499 | + search_base = 'ou=accounts,ou=caedm,dc=et,dc=byu,dc=edu' |
| 500 | + conn.search( |
| 501 | + search_base, |
| 502 | + search_filter, |
| 503 | + SUBTREE, |
| 504 | + attributes=['cn', 'displayName', 'CAEDMUserType'], |
| 505 | + time_limit=int(self.connection_timeout) |
| 506 | + ) |
| 507 | + |
| 508 | + if conn.entries: |
| 509 | + display_name = conn.entries[0].displayName.value |
| 510 | + user_group = conn.entries[0].CAEDMUserType.value |
| 511 | + return User(ident, realname=display_name, usergroup=user_group, authmethod='caedm') |
| 512 | + except LDAPSocketReceiveError as e: |
| 513 | + print(f'LDAP search timed out for user {ident}: {e}') |
| 514 | + finally: |
| 515 | + conn.unbind() |
| 516 | + |
| 517 | + print(f'No CAEDM user "{ident}" found') |
| 518 | + return None |
| 519 | + |
| 520 | + |
358 | 521 | class UserManager:
|
359 | 522 | """
|
360 | 523 | Manages user operations including authentication, database interactions, and request tracking.
|
|
0 commit comments