Skip to content

Commit 712f138

Browse files
committed
mutliple changes
-added CAEDM authenticator -modified .gitignore to ignore user specific files -removed requirements-gpu.txt and merged it with requirements.txt -added ldap 3 to projects dependencies for increased authentication support
1 parent 2e0ae40 commit 712f138

File tree

5 files changed

+175
-2
lines changed

5 files changed

+175
-2
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,8 @@ logs
1818
chat_logs
1919
# Ignore package build
2020
dist
21+
#Ignore poetry.lock files
22+
poetry.lock
23+
#Ignore
24+
*.faiss
25+
*.pkl

maeser/user_manager.py

+163
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
from abc import ABC, abstractmethod
2727
from typing import Any, Tuple, Union
2828
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
2934

3035
import requests
3136

@@ -174,6 +179,7 @@ def form_html(self) -> str:
174179
def form_html(self, html: str):
175180
self._custom_form = html
176181

182+
177183
class BaseAuthenticator(ABC):
178184
"""
179185
Base class for authenticators.
@@ -355,6 +361,163 @@ def get_auth_info(self) -> Tuple[str, str]:
355361
return oauth_state, provider_url
356362

357363

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+
358521
class UserManager:
359522
"""
360523
Manages user operations including authentication, database interactions, and request tracking.

pyproject.toml

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ faiss-cpu = "^1.8.0.post1"
3232
flask = "^3.0.3"
3333
flask-login = "^0.6.3"
3434

35+
#Ldap3 for alternative authentication purposes
36+
ldap3 = "^2.9.1"
37+
3538
# Turning markdown into HTML for display
3639
markdown = "^3.6"
3740
pymdown-extensions = "^10.8.1"

requirements/requirements-gpu.txt

-2
This file was deleted.

requirements/requirements.txt

+4
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ langgraph
99
# FAISS for vector stores (choose either faiss-cpu or faiss-gpu depending on your setup)
1010
faiss-cpu
1111
# faiss-gpu
12+
faiss-gpu
1213

1314
# Web backend
1415
flask
1516
flask-login
1617

18+
#Caedm Authenticator
19+
ldap3
20+
1721
# Turning markdown into HTML for display
1822
markdown
1923
pymdown-extensions

0 commit comments

Comments
 (0)