mirror of
ssh://git.janware.com/srv/git/janware/proj/jw-python
synced 2026-01-15 01:52:56 +01:00
auth: Add LDAP support
Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
parent
0a1a6e5e17
commit
8a316ead21
4 changed files with 283 additions and 29 deletions
|
|
@ -1,11 +1,13 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union, Self
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
|
||||||
from enum import Enum, auto
|
from enum import Flag, Enum, auto
|
||||||
from jwutils import log, Config
|
|
||||||
|
from jwutils.log import *
|
||||||
|
from jwutils import Config, load_object
|
||||||
|
|
||||||
class Access(Enum): # export
|
class Access(Enum): # export
|
||||||
Read = auto()
|
Read = auto()
|
||||||
|
|
@ -13,6 +15,11 @@ class Access(Enum): # export
|
||||||
Create = auto()
|
Create = auto()
|
||||||
Delete = auto()
|
Delete = auto()
|
||||||
|
|
||||||
|
class ProjectFlags(Flag): # export
|
||||||
|
NoFlags = auto()
|
||||||
|
Contributing = auto()
|
||||||
|
Active = auto()
|
||||||
|
|
||||||
class Group: # export
|
class Group: # export
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
|
@ -33,40 +40,94 @@ class User: # export
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def _name(self) -> str:
|
def _name(self) -> str:
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _groups(self) -> list[Group]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return self._name()
|
return self._name()
|
||||||
|
|
||||||
|
def _display_name(self) -> str:
|
||||||
|
return self._name()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_name(self) -> str:
|
||||||
|
return self._display_name()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _groups(self) -> list[Group]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def groups(self) -> list[Group]:
|
def groups(self) -> list[Group]:
|
||||||
return self._groups()
|
return self._groups()
|
||||||
|
|
||||||
class Auth: # export
|
@abc.abstractmethod
|
||||||
|
def _email(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def email(self) -> str:
|
||||||
|
return self._email()
|
||||||
|
|
||||||
|
class Auth(abc.ABC): # export
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, tp: str, conf: Config) -> Self:
|
||||||
|
return load_object(f'jwutils.auth.{tp}.Auth', Auth, 'Auth', conf)
|
||||||
|
|
||||||
def __init__(self, conf: Config):
|
def __init__(self, conf: Config):
|
||||||
self.__conf = conf
|
self.__conf = conf
|
||||||
|
self.__base_user_by_email: Optional[dict[str, User]] = None
|
||||||
@abc.abstractmethod
|
|
||||||
def _access(self, what: str, access_type: Optional[Access], who: User|Group|None) -> bool:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _current_user(self) -> User:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def conf(self):
|
def conf(self):
|
||||||
return self.__conf
|
return self.__conf
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _access(self, what: str, access_type: Optional[Access], who: User|Group|None) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def access(self, what: str, access_type: Optional[Access]=None, who: Optional[Union[User|Group]]=None) -> bool:
|
||||||
|
return self._access(what, access_type, who)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _current_user(self) -> User:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_user(self) -> User:
|
def current_user(self) -> User:
|
||||||
return self._current_user()
|
return self._current_user()
|
||||||
|
|
||||||
def access(self, what: str, access_type: Optional[Access]=None, who: Optional[Union[User|Group]]=None) -> bool:
|
@abc.abstractmethod
|
||||||
return self._access(what, access_type, who)
|
def _user(self, name) -> User:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def user(self, name) -> User:
|
||||||
|
return self._user(name)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _users(self) -> list[User]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _user_by_email(self, email: str) -> User:
|
||||||
|
if self.__base_user_by_email is None:
|
||||||
|
ret: dict[str, User] = dict()
|
||||||
|
users = self._users()
|
||||||
|
for user in users.values():
|
||||||
|
ret[user.email] = user
|
||||||
|
self.__base_user_by_email = ret
|
||||||
|
return self.__base_user_by_email[email]
|
||||||
|
|
||||||
|
def user_by_email(self, email) -> User:
|
||||||
|
return self._user_by_email(email)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def users(self) -> list[User]:
|
||||||
|
return self._users()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _projects(self, name, flags: ProjectFlags) -> list[str]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def projects(self, name, flags: ProjectFlags) -> list[str]:
|
||||||
|
return self._projects(name, flags)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from .. import Access
|
||||||
from .. import Auth as AuthBase
|
from .. import Auth as AuthBase
|
||||||
from .. import Group as GroupBase
|
from .. import Group as GroupBase
|
||||||
from .. import User as UserBase
|
from .. import User as UserBase
|
||||||
|
from .. import ProjectFlags
|
||||||
|
|
||||||
class Group(GroupBase): # export
|
class Group(GroupBase): # export
|
||||||
|
|
||||||
|
|
@ -18,12 +19,18 @@ class Group(GroupBase): # export
|
||||||
def _name(self) -> str:
|
def _name(self) -> str:
|
||||||
return self.__name
|
return self.__name
|
||||||
|
|
||||||
class User(UserBase):
|
class User(UserBase): # export
|
||||||
|
|
||||||
def __init__(self, auth: AuthBase, name: str):
|
def __init__(self, auth: AuthBase, name: str, conf: Config):
|
||||||
self.__name = name
|
self.__name = name
|
||||||
|
self.__conf = conf
|
||||||
self.__auth = auth
|
self.__auth = auth
|
||||||
self.__groups: Optional[list[GroupBase]] = None
|
self.__groups: Optional[list[GroupBase]] = None
|
||||||
|
self.__email = conf.get('email')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def conf(self):
|
||||||
|
return self.__conf
|
||||||
|
|
||||||
def _name(self) -> str:
|
def _name(self) -> str:
|
||||||
return self.__name
|
return self.__name
|
||||||
|
|
@ -32,32 +39,54 @@ class User(UserBase):
|
||||||
if self.__groups is None:
|
if self.__groups is None:
|
||||||
name: str = ''
|
name: str = ''
|
||||||
ret: list[GroupBase] = []
|
ret: list[GroupBase] = []
|
||||||
for name in self.__auth.conf['user.' + name + '.groups']:
|
for name in self.conf['groups']:
|
||||||
ret.append(Group(self.__auth, name))
|
ret.append(Group(self.__auth, name))
|
||||||
self.__groups = ret
|
self.__groups = ret
|
||||||
return self.__groups
|
return self.__groups
|
||||||
|
|
||||||
|
def _email(self) -> str:
|
||||||
|
return self.__email
|
||||||
|
|
||||||
class Auth(AuthBase): # export
|
class Auth(AuthBase): # export
|
||||||
|
|
||||||
def __init__(self, conf: Config):
|
def __init__(self, conf: Config):
|
||||||
self.__conf = conf
|
super().__init__(conf)
|
||||||
self.__users: Optional[dict[str, User]] = None
|
self.___users: Optional[dict[str, User]] = None
|
||||||
self.__groups = None
|
self.__groups = None
|
||||||
self.__current_user: User|None = None
|
self.__current_user: User|None = None
|
||||||
|
|
||||||
def _user(self, name_) -> User:
|
@property
|
||||||
if self.__users is None:
|
def __users(self) -> User:
|
||||||
|
if self.___users is None:
|
||||||
ret: dict[str, User] = {}
|
ret: dict[str, User] = {}
|
||||||
for name in self.conf.entries('user'):
|
for name in self.conf.entries('user'):
|
||||||
ret[name] = User(self, name)
|
conf = self.conf.branch('user.' + name)
|
||||||
self.__users = ret
|
ret[name] = User(self, name, conf)
|
||||||
return self.__users[name_]
|
self.___users = ret
|
||||||
|
return self.___users
|
||||||
|
|
||||||
def _access(self, what: str, access_type: Optional[Access], who: User|GroupBase|None) -> bool: # type: ignore
|
def _access(self, what: str, access_type: Optional[Access], who: User|GroupBase|None) -> bool: # type: ignore
|
||||||
slog(WARNING, f'Returning False for {access_type} access to resource {what} by {who}')
|
slog(WARNING, f'Returning False for {access_type} access to resource {what} by {who}')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _user(self, name) -> User:
|
||||||
|
return self.__users[name]
|
||||||
|
|
||||||
|
def _users(self) -> list[User]:
|
||||||
|
return self.__users
|
||||||
|
|
||||||
def _current_user(self) -> User:
|
def _current_user(self) -> User:
|
||||||
if self.__current_user is None:
|
if self.__current_user is None:
|
||||||
self.__current_user = self._user(self.conf['current_user'])
|
self.__current_user = self._user(self.conf['current_user'])
|
||||||
return self.__current_user
|
return self.__current_user
|
||||||
|
|
||||||
|
def _user_by_email(self, email: str) -> User:
|
||||||
|
if self.__user_by_email is None:
|
||||||
|
ret: dict[str, User] = dict()
|
||||||
|
for user in self.__users.values():
|
||||||
|
ret[user.email] = user
|
||||||
|
self.__user_by_email = ret
|
||||||
|
return self.__user_by_email[email]
|
||||||
|
|
||||||
|
def _projects(self, name, flags: ProjectFlags) -> list[str]:
|
||||||
|
return None
|
||||||
|
|
|
||||||
160
tools/python/jwutils/auth/ldap/Auth.py
Normal file
160
tools/python/jwutils/auth/ldap/Auth.py
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
import ldap
|
||||||
|
|
||||||
|
from ...log import *
|
||||||
|
from ... import Config
|
||||||
|
from .. import Access
|
||||||
|
from .. import Auth as AuthBase
|
||||||
|
from .. import Group as GroupBase
|
||||||
|
from .. import User as UserBase
|
||||||
|
from .. import ProjectFlags
|
||||||
|
|
||||||
|
class Group(GroupBase): # export
|
||||||
|
|
||||||
|
def __init__(self, auth: AuthBase, name: str):
|
||||||
|
self.__name = name
|
||||||
|
self.__auth = auth
|
||||||
|
|
||||||
|
def _name(self) -> str:
|
||||||
|
return self.__name
|
||||||
|
|
||||||
|
class User(UserBase):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
auth: AuthBase,
|
||||||
|
name: str,
|
||||||
|
cn: str,
|
||||||
|
email: str
|
||||||
|
):
|
||||||
|
|
||||||
|
self.__auth = auth
|
||||||
|
self.__name = name
|
||||||
|
self.__cn = cn
|
||||||
|
self.__email = email
|
||||||
|
self.__groups: Optional[list[GroupBase]] = None
|
||||||
|
|
||||||
|
def _name(self) -> str:
|
||||||
|
return self.__name
|
||||||
|
|
||||||
|
def _groups(self) -> list[GroupBase]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _email(self) -> str:
|
||||||
|
return self.__email
|
||||||
|
|
||||||
|
def _display_name(self) -> str:
|
||||||
|
return self.__cn
|
||||||
|
|
||||||
|
class Auth(AuthBase): # export
|
||||||
|
|
||||||
|
def __init__(self, conf: Config):
|
||||||
|
super().__init__(conf)
|
||||||
|
self.___users: Optional[dict[str, User]] = None
|
||||||
|
self.___user_by_email: Optional[dict[str, User]] = None
|
||||||
|
self.__groups = None
|
||||||
|
self.__current_user: User|None = None
|
||||||
|
self.__user_base_dn = conf['user_base_dn']
|
||||||
|
self.__conn = self.__bind()
|
||||||
|
self.__dummy = self.load('dummy', conf)
|
||||||
|
|
||||||
|
def __bind(self):
|
||||||
|
ldap_uri = self.conf['ldap_uri']
|
||||||
|
bind_dn = self.conf['bind_dn']
|
||||||
|
bind_pw = self.conf.get('password')
|
||||||
|
if bind_pw is None:
|
||||||
|
with open(ldap_secret_file, 'r') as file:
|
||||||
|
bind_pw = file.read()
|
||||||
|
file.closed
|
||||||
|
bind_pw = bind_pw.strip()
|
||||||
|
ret = ldap.initialize(ldap_uri)
|
||||||
|
ret.start_tls_s()
|
||||||
|
try:
|
||||||
|
rr = ret.bind_s(bind_dn, bind_pw) # method)
|
||||||
|
except Exception as e:
|
||||||
|
#pw = f' (pw={bind_pw})'
|
||||||
|
raise Exception(f'Failed to bind to {ldap_uri} with dn {bind_dn} ({e})')
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@property
|
||||||
|
def __users(self) -> User:
|
||||||
|
if self.___users is None:
|
||||||
|
ret: dict[str, User] = {}
|
||||||
|
ret_by_email: dict[str, User] = {}
|
||||||
|
ldap_result_id = self.__conn.search(
|
||||||
|
self.__user_base_dn,
|
||||||
|
ldap.SCOPE_SUBTREE,
|
||||||
|
"objectClass=inetOrgPerson",
|
||||||
|
('uid', 'cn', 'uidNumber', 'mail', 'maildrop')
|
||||||
|
)
|
||||||
|
while True:
|
||||||
|
result_type, result_data = self.__conn.result(ldap_result_id, 0)
|
||||||
|
if (result_data == []):
|
||||||
|
break
|
||||||
|
if result_type != ldap.RES_SEARCH_ENTRY:
|
||||||
|
continue
|
||||||
|
for res in result_data:
|
||||||
|
try:
|
||||||
|
display_name = None
|
||||||
|
if 'displayName' in res[1]:
|
||||||
|
cn = res[1]['displayName'][0].decode('utf-8')
|
||||||
|
else:
|
||||||
|
cn = res[1]['cn'][0].decode('utf-8')
|
||||||
|
uid = res[1]['uid'][0].decode('utf-8')
|
||||||
|
uidNumber = res[1]['uidNumber'][0].decode('utf-8')
|
||||||
|
emails = []
|
||||||
|
#for attr in ['mail', 'maildrop']:
|
||||||
|
for attr in ['mail']:
|
||||||
|
if attr in res[1]:
|
||||||
|
for entry in res[1][attr]:
|
||||||
|
emails.append(entry.decode('utf-8'))
|
||||||
|
if not emails:
|
||||||
|
slog(DEBUG, f'No email for user "{uid}", skipping')
|
||||||
|
continue
|
||||||
|
user = User(self, name=uid, cn=cn, email=emails[0])
|
||||||
|
ret[uid] = user
|
||||||
|
for email in emails:
|
||||||
|
ret_by_email[email] = user
|
||||||
|
except Exception as e:
|
||||||
|
slog(WARNING, f'Exception {e}')
|
||||||
|
continue
|
||||||
|
for user in self.__dummy.users.values():
|
||||||
|
ret[user.name] = user
|
||||||
|
self.___users = ret
|
||||||
|
self.___user_by_email = ret_by_email
|
||||||
|
return self.___users
|
||||||
|
|
||||||
|
@property
|
||||||
|
def __user_by_email(self) -> User:
|
||||||
|
if self.___user_by_email is None:
|
||||||
|
self.__users
|
||||||
|
return self.___user_by_email
|
||||||
|
|
||||||
|
def _access(self, what: str, access_type: Optional[Access], who: User|GroupBase|None) -> bool: # type: ignore
|
||||||
|
slog(WARNING, f'Returning False for {access_type} access to resource {what} by {who}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _user(self, name) -> User:
|
||||||
|
try:
|
||||||
|
return self.__users[name]
|
||||||
|
except:
|
||||||
|
slog(ERR, f'No such user: "{name}"')
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _user_by_email(self, email: str) -> User:
|
||||||
|
return self.__user_by_email[email]
|
||||||
|
|
||||||
|
def _current_user(self) -> User:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _users(self) -> list[User]:
|
||||||
|
return self.__users
|
||||||
|
|
||||||
|
def _projects(self, name, flags: ProjectFlags) -> list[str]:
|
||||||
|
if flags & ProjectFlags.Contributing:
|
||||||
|
# TODO: Ask LDAP
|
||||||
|
pass
|
||||||
|
return None
|
||||||
4
tools/python/jwutils/auth/ldap/Makefile
Normal file
4
tools/python/jwutils/auth/ldap/Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
TOPDIR = ../../../../..
|
||||||
|
|
||||||
|
include $(TOPDIR)/make/proj.mk
|
||||||
|
include $(JWBDIR)/make/py-mod.mk
|
||||||
Loading…
Add table
Add a link
Reference in a new issue