ebuffer 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ebuffer/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.5"
ebuffer/apiebuffer.py ADDED
@@ -0,0 +1,80 @@
1
+ from io import BytesIO
2
+ from asyncio import create_task
3
+ from sqlmodel import Session
4
+ from typing import AsyncGenerator
5
+ from ebuffer.database import get_user_db
6
+ from ebuffer.apiuserobj import Eb_UserObjAPI, Eb_ObjectInvalid
7
+
8
+ from ebuffer.config import EbConfig, logger
9
+ from ebuffer.models_common import UserId
10
+ from ebuffer.models_buffer import BufferStateEnum
11
+ from ebuffer.database import BufferEntry, Eb_BufferCRUD
12
+ from ebuffer.backends import Eb_DataBackend, Eb_BackendReadError
13
+
14
+ Eb_BufferInvalid = Eb_ObjectInvalid
15
+
16
+ class Eb_BufferAPI(Eb_UserObjAPI):
17
+
18
+ def __init__(self, config: EbConfig):
19
+ self.config = config
20
+ self.bufferCRUD = Eb_BufferCRUD()
21
+ self.backend = Eb_DataBackend.getBackend(self.config, self.bufferCRUD)
22
+ super().__init__(BufferEntry, config.base, self.bufferCRUD, self.backend)
23
+
24
+ def destroy(self):
25
+ self.backend.deep_clean()
26
+ del self.backend
27
+
28
+ #
29
+ # Housekeeping
30
+ #
31
+ async def housekeeping(self, session: Session, interval: int, start: bool = False) -> None:
32
+ grace_time = interval - 1
33
+
34
+ for ebuffer in self.bufferCRUD.get_expired(session, interval):
35
+ if ebuffer.state == BufferStateEnum.error_deleted or ebuffer.state == BufferStateEnum.deleted:
36
+ if not start: continue
37
+
38
+ if ebuffer.state == BufferStateEnum.initialized:
39
+ ebuffer.state = BufferStateEnum.error
40
+ ebuffer.state_desc = r'Never reached backend initialization.' if not ebuffer.state_desc else ebuffer.state_desc
41
+ elif ebuffer.state == BufferStateEnum.error:
42
+ grace_time = self.config.base.error_grace_time
43
+ if ebuffer.lifetime < grace_time:
44
+ ebuffer.state = BufferStateEnum.error_deleted
45
+ else:
46
+ if ebuffer.lifetime < grace_time:
47
+ ebuffer.state = BufferStateEnum.deleted
48
+
49
+ if ebuffer.state == BufferStateEnum.error_deleted or ebuffer.state == BufferStateEnum.deleted:
50
+ logger.debug("[housekeeping]: Destroy (%ds < %ds) %s", ebuffer.lifetime, grace_time, ebuffer)
51
+ await self.bufferCRUD.update(session, ebuffer, commit=False)
52
+ await self.destroy_after(session, grace_time, ebuffer, commit=False)
53
+ self.bufferCRUD.commit(session)
54
+
55
+ #
56
+ # I/O operations
57
+ #
58
+ async def write(self, stream: AsyncGenerator[bytes, None], uid: str, user: UserId, session: Session) -> object:
59
+ #userd = get_user_db(session, user, create=False)
60
+ ebuffer = self.bufferCRUD.get(session, uid, user) # Warning: the for now, data has to belong to the user.
61
+ try:
62
+ await self.backend.write(ebuffer, stream)
63
+ finally:
64
+ create_task(self.bufferCRUD.update(session, ebuffer))
65
+ return ebuffer
66
+
67
+ def _generate_bytesio_stream(self, data: BytesIO, chunk_size: int = 4 * 1024 * 1024) -> bytes:
68
+ while chunk := data.read(chunk_size):
69
+ yield chunk # Envoie un morceau au client
70
+ data.close()
71
+
72
+ async def read(self, uid: str, sid: str, seek: int, limit: int, user: UserId, session: Session) -> object:
73
+ # Warning : 'user' is not used, but shall be in order to check if the data has read policy.
74
+ # userd : UserIdEntry = get_user_db(session, user, create=False)
75
+ ebuffer = self.bufferCRUD.get(session, uid)
76
+ data: BytesIO; clen: int
77
+ clen, data = self.backend.fetch(ebuffer, sid, seek=seek, limit=limit) if sid else self.backend.fopen(ebuffer, seek=seek, limit=limit)
78
+ if not data:
79
+ raise Eb_BackendReadError(ebuffer.uuid, r'Empty data stream')
80
+ return clen, self._generate_bytesio_stream(data)
ebuffer/apiuserobj.py ADDED
@@ -0,0 +1,127 @@
1
+ from sqlmodel import Session
2
+ from asyncio import create_task, sleep
3
+
4
+ from ebuffer.config import logger
5
+ from ebuffer.models_common import UserId
6
+ from ebuffer.database import UserIdEntry, UserObjEntry, get_user_db, get_user_by_email_db
7
+ from ebuffer.database import Eb_UserObjCRUD, Eb_ObjectNotFound
8
+ from ebuffer.errors import Eb_Exception, Eb_TagSyntaxError
9
+
10
+ #
11
+ # CRUD Management of User's objects
12
+
13
+ class Eb_ObjectInvalid(Eb_Exception):
14
+ def __init__(self, uuid: str = r''):
15
+ super().__init__(430, "ObjectInvalid", "Object is invalid or was destroyed.", r'Id: %s' % uuid)
16
+
17
+ class Eb_UserObjAPI():
18
+
19
+ def __init__(self, Objtype: type, config: object, uobjcrud: Eb_UserObjCRUD, backend: object = None):
20
+ self.ObjEntry: type = Objtype
21
+ self.conf = config
22
+ self.uobjcrud = uobjcrud
23
+ self.backend = backend
24
+
25
+ #
26
+ # Life cycle management
27
+
28
+ def assert_correct_tags(self, tags: list[str]) -> bool:
29
+ tag_min_size = self.conf.tag_min_size
30
+ tag_max_size = self.conf.tag_max_size
31
+ for t in tags:
32
+ if len(t) < tag_min_size or len(t) > tag_max_size:
33
+ raise Eb_TagSyntaxError(r'Invalid tag size %d ([%d:%d])' % (len(t), tag_min_size, tag_max_size))
34
+ return True
35
+
36
+ async def _destroy(self, session: Session, uobj: UserObjEntry, commit) -> None:
37
+ if self.backend: await self.backend.destroy(session, uobj, commit=commit)
38
+ else: await self.uobjcrud.destroy(session, uobj, commit=commit)
39
+
40
+ async def _destroy_after(self, session: Session, time_s: int, uobj: UserObjEntry, maxcount: int, commit) -> None:
41
+ try:
42
+ await sleep(time_s)
43
+ # session: Session = next(app.g_db.get_session())
44
+ await self._destroy(session, uobj, commit)
45
+ except Exception as e:
46
+ logger.debug(r"Could not finish a destroy operation: %s", str(e))
47
+ if maxcount > 0:
48
+ create_task(self._destroy_after(session, time_s, uobj, maxcount-1, commit))
49
+
50
+ async def destroy_after(self, session: Session, time_s: int, uobj: UserObjEntry, maxcount: int = 5, commit=True) -> None:
51
+ #create_task(self._destroy_after(session, time_s, uobj, maxcount, commit=True))
52
+ if time_s: create_task(self._destroy_after(session, time_s, uobj, maxcount, commit=True))
53
+ else: self._destroy(session, uobj, commit)
54
+
55
+ #
56
+ # Main API
57
+
58
+ async def create(self, uobj: UserObjEntry, blocking: bool, user: UserId, session: Session) -> UserObjEntry:
59
+ self.assert_correct_tags(uobj.tags)
60
+ # Only in order to register the used in the DB.
61
+ userId: UserIdEntry = get_user_db(session, user, create=True)
62
+ uobj.user_email = userId.email
63
+
64
+ if blocking and self.backend:
65
+ await self.backend.build(session, uobj)
66
+ else:
67
+ await self.uobjcrud.update(session, uobj)
68
+ if self.backend:
69
+ create_task(self.backend.build(session, uobj))
70
+ logger.debug(r'[api] Register %s: %s', self.ObjEntry.__name__, uobj)
71
+ return uobj
72
+
73
+ async def search(self, session: Session = None, user: UserId = None,
74
+ limit: int = 0, skip: int = 0,
75
+ owner: str = None, tags: list[str] = [], all: bool = False, count: bool = False,
76
+ ofilter: callable = None) -> list[UserObjEntry] | int:
77
+ self.assert_correct_tags(tags)
78
+ if owner:
79
+ owner = get_user_by_email_db(session, owner, create=False)
80
+ if not owner:
81
+ return 0 if count else []
82
+ results = []
83
+
84
+ # Warning : 'user' is not used, but shall be in order to check if the data has read policy.
85
+ if count:
86
+ #logger.debug("[searchObject]------ %s: %s [%s] --------", str(owner), str(tags), str(all))
87
+ return self.uobjcrud.count(session, limit, skip, owner, tags, all, ofilter=ofilter)
88
+ else:
89
+ #logger.debug("[searchObject]------ %s: %s [%s] --------", str(owner), str(tags), str(all))
90
+ for uobj in self.uobjcrud.search(session, limit, skip, owner, tags, all, ofilter=ofilter):
91
+ # logger.debug("[searchObject]: found %s" % uobj)
92
+ results.append(uobj)
93
+ # logger.debug("[searchObject]+++++++++++++++")
94
+ return results
95
+
96
+ async def get(self, uid: str, owner: str, user: UserId, session: Session) -> UserObjEntry:
97
+ if owner:
98
+ owner = get_user_by_email_db(session, owner, create=False)
99
+ if not owner: raise Eb_ObjectNotFound(uid)
100
+ # Warning : 'user' is not used, but shall be in order to check if the data has read policy.
101
+ return self.uobjcrud.get(session, uid, owner)
102
+
103
+ async def delete(self, uid: str, user: UserId, session: Session) -> UserObjEntry:
104
+ #userd = get_user_db(session, user, create=False)
105
+ uobj = self.uobjcrud.get(session, uid, user) # Warning: the for now, data has to belong to the user.
106
+ if uobj.is_deleted(): return uobj
107
+ uobj.set_deleted()
108
+ await self.uobjcrud.update(session, uobj)
109
+ await self.destroy_after(session, self.conf.grace_time, uobj)
110
+ return uobj
111
+
112
+ #
113
+ # Tag API
114
+
115
+ async def getTags(self, uid: str, owner: str, user: UserId, session: Session) -> UserObjEntry:
116
+ uobj = await self.get(uid, owner, user, session)
117
+ return uobj.tags if uobj else []
118
+
119
+ async def addTag(self, uid: str, tag: str, user: UserId, session: Session) -> UserObjEntry:
120
+ #userd = get_user_db(session, user, create=False)
121
+ self.assert_correct_tags((tag,))
122
+ uobj = self.uobjcrud.get(session, uid, user) # Warning: the for now, data has to belong to the user.
123
+ if uobj.is_deleted(): raise Eb_ObjectInvalid(uid)
124
+ uobj.tags = list(uobj.tags)
125
+ uobj.tags.append(tag)
126
+ create_task(self.uobjcrud.update(session, uobj))
127
+ return uobj
ebuffer/application.py ADDED
@@ -0,0 +1,45 @@
1
+ from os import getenv
2
+ from asyncio import create_task, sleep
3
+ from traceback import format_exc
4
+ from ebuffer.config import EbConfig, logger
5
+ from ebuffer.database import Eb_Database
6
+ from ebuffer.auth import Eb_Auth
7
+ from ebuffer.apiebuffer import Eb_BufferAPI
8
+ from fastapi.security import HTTPBearer, HTTPBasic
9
+
10
+ class Eb_Application():
11
+
12
+ def __init__(self):
13
+ self.regexp_email = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
14
+ self.config = EbConfig(inifile=getenv(r'EBUFFER_INI', r'eb.ini'), secretfile=getenv(r'EBUFFER_SECRET_INI', r'.eb-secret.ini'))
15
+ self.db = Eb_Database(self.config)
16
+ self.bufferAPI = Eb_BufferAPI(self.config)
17
+ self.auth = Eb_Auth.getAuth(self.config)
18
+ self.security_token = HTTPBearer(auto_error=False)
19
+ self.security_cred = HTTPBasic(auto_error=False)
20
+
21
+ #
22
+ # Housekeeping
23
+ #
24
+ async def housekeeping(self, start: bool = False) -> None:
25
+ interval = self.config.base.housekeeping_interval
26
+ await sleep(interval)
27
+ #logger.debug(r"Start housekeeping (%ds) %s", interval, r'Initial' if start else r'')
28
+ try:
29
+ session = next(self.db.get_session())
30
+ await self.bufferAPI.housekeeping(session, interval, start)
31
+ except Exception as e:
32
+ logger.error(r"Could not finish a housekeeping session %s: %s", str(e), format_exc())
33
+ # raise Eb_HouseKeepingError(e)
34
+ finally:
35
+ create_task(self.housekeeping())
36
+
37
+ def start(self):
38
+ create_task(self.housekeeping(start=True))
39
+
40
+ def destroy(self):
41
+ self.bufferAPI.destroy()
42
+ del self.db
43
+ self.db = None
44
+
45
+ app_g = Eb_Application()
@@ -0,0 +1,15 @@
1
+ from ebuffer.auth.auth import Eb_Auth
2
+
3
+ from ebuffer.auth.auth import Eb_AuthDuplicatedError, Eb_AuthMissingError
4
+ from ebuffer.auth.auth import Eb_AuthError, Eb_AuthExpired
5
+ from ebuffer.auth.openid import Eb_OI_ConnectError, Eb_OI_InternalError, Eb_OI_LoginError
6
+ from ebuffer.auth.passfile import Eb_AuthFile_LoginError
7
+
8
+ from ebuffer.auth.openid import OIDLoginExceptionList
9
+ from ebuffer.auth.openid import OIDAuthExceptionList
10
+
11
+ from ebuffer.auth.passfile import PassFileLoginExceptionList
12
+ from ebuffer.auth.passfile import PassFileAuthExceptionList
13
+
14
+ LoginExceptionList = {} | OIDLoginExceptionList | PassFileLoginExceptionList
15
+ AuthExceptionList = {} | OIDAuthExceptionList | PassFileAuthExceptionList
ebuffer/auth/auth.py ADDED
@@ -0,0 +1,51 @@
1
+ from ebuffer.config import EbConfig
2
+ from ebuffer.models_common import UserCred, UserId, MessageLogin
3
+ from ebuffer.errors import Eb_Exception
4
+
5
+ class Eb_AuthError(Eb_Exception):
6
+ def __init__(self, code: int = -1):
7
+ super().__init__(401, "AuthError", "Invalid credentials.", r'Code: %d' % code)
8
+
9
+ class Eb_AuthExpired(Eb_Exception):
10
+ def __init__(self, status: str = r''):
11
+ super().__init__(401, "AuthExpired", "Invalid or expired token.", r'Status: %s' % status)
12
+
13
+ class Eb_AuthDuplicatedError(Eb_Exception):
14
+ def __init__(self, scheme: str):
15
+ super().__init__(440, "AuthDuplicatedError", "Multiple authentication defined with the same scheme (%s)." % scheme, "Check authentication installation.")
16
+
17
+ class Eb_AuthMissingError(Eb_Exception):
18
+ def __init__(self, scheme: str):
19
+ super().__init__(440, "AuthMissingError", "Authentication '%s' not found." % scheme, "Check authentication installation.")
20
+
21
+ class Eb_Auth():
22
+ g_auths: dict = {}
23
+
24
+ def __init__(self, config: EbConfig, name : str = r'none'):
25
+ self.config = config
26
+ self.name : str = name
27
+
28
+ @staticmethod
29
+ def addAuth(name, auth_class):
30
+ if name in Eb_Auth.g_auths: raise Eb_AuthDuplicatedError(name)
31
+ Eb_Auth.g_auths[name] = auth_class
32
+
33
+ @staticmethod
34
+ def getAuth(config: EbConfig):
35
+ name = config.base.auth_backend
36
+ if name not in Eb_Auth.g_auths: raise Eb_AuthMissingError(name)
37
+ return Eb_Auth.g_auths[name](config, name)
38
+
39
+ def login(self, user: UserCred | None, headers: dict) -> MessageLogin:
40
+ return MessageLogin(access_token=r'b3BlbmJhcg==')
41
+
42
+ def verify_token(self, scheme: str, token: str) -> UserId:
43
+ return UserId(name=self.config.base.default_user, email=self.config.base.default_email)
44
+
45
+ def verify_basic(self, user: str, password: str) -> UserId:
46
+ return UserId(name=self.config.base.default_user, email=self.config.base.default_email)
47
+
48
+ def getRealmHeader(self) -> dict:
49
+ return {}
50
+
51
+ Eb_Auth.addAuth(r'none', Eb_Auth)
ebuffer/auth/openid.py ADDED
@@ -0,0 +1,101 @@
1
+ import requests
2
+ from pydantic import BaseModel
3
+ from ebuffer.models_common import UserCred, UserId, MessageLogin
4
+ from ebuffer.config import EbConfig, logger
5
+ from ebuffer.errors import Eb_Exception
6
+ from ebuffer.auth import Eb_Auth, Eb_AuthError, Eb_AuthExpired
7
+
8
+ class Eb_OI_ConnectError(Eb_Exception):
9
+ def __init__(self, srv: str = r'', exc: Exception = None):
10
+ super().__init__(501, "OIDConnErr", "Could not connect to the OpenID server.", r'Server: %s' % srv, exc)
11
+
12
+ class Eb_OI_InternalError(Eb_Exception):
13
+ def __init__(self, exc: Exception = None):
14
+ super().__init__(500, "OIDInternalError", "OIDC Internal error.", "Check server configuration.", exc)
15
+
16
+ class Eb_OI_LoginError(Eb_Exception):
17
+ def __init__(self, code: int = -1):
18
+ super().__init__(401, "OIDLoginErr", "Could not login with OIDC credentials.", r'Code: %d' % code)
19
+
20
+ OIDLoginExceptionList = \
21
+ Eb_OI_InternalError().model() | \
22
+ Eb_OI_ConnectError().model() | \
23
+ Eb_OI_LoginError().model()
24
+
25
+ OIDAuthExceptionList = \
26
+ Eb_OI_InternalError().model() | \
27
+ Eb_AuthError().model() | \
28
+ Eb_AuthExpired().model()
29
+
30
+ class EbConfigOpenID(BaseModel):
31
+ openid_base_url: str = r''
32
+ openid_client_id: str = r''
33
+ openid_client_secret: str = r''
34
+ access_token_url: str
35
+ introspect_url: str
36
+ user_info_url: str
37
+ logout_url: str
38
+
39
+ def __init__(self, **kw):
40
+ url = kw[r'openid_base_url'] if r'openid_base_url' in kw else r'https://localhost/realms'
41
+ kw[r'access_token_url'] = f"{url}/protocol/openid-connect/token"
42
+ kw[r'introspect_url'] = f"{url}/protocol/openid-connect/token/introspect"
43
+ kw[r'user_info_url'] = f"{url}/protocol/openid-connect/userinfo"
44
+ kw[r'logout_url'] = f"{url}/protocol/openid-connect/logout"
45
+ super().__init__(**kw)
46
+
47
+ class Eb_AuthOpenID(Eb_Auth):
48
+
49
+ def __init__(self, config: EbConfig, name : str):
50
+ config = config.open_section(name, EbConfigOpenID)
51
+ super().__init__(config, name)
52
+
53
+ def login(self, user: UserCred | None, headers: dict) -> MessageLogin:
54
+ logger.debug(r"----- OpenID %s ------", str(user))
55
+ payload = {
56
+ "client_id": self.config.openid_client_id,
57
+ "client_secret": self.config.openid_client_secret,
58
+ "grant_type": "password",
59
+ "username": user.username,
60
+ "password": user.password,
61
+ "scope": "openid",
62
+ }
63
+
64
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
65
+ try:
66
+ response = requests.post(self.config.access_token_url, data=payload, headers=headers)
67
+ except requests.exceptions.ConnectionError as e:
68
+ raise Eb_OI_ConnectError(self.config.access_token_url, e)
69
+
70
+ if response.status_code != 200:
71
+ raise Eb_OI_LoginError(response.status_code)
72
+
73
+ result = response.json()
74
+ return MessageLogin(result["access_token"])
75
+
76
+ def verify_token(self, scheme: str, token: str) -> UserId:
77
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
78
+ payloads = {
79
+ "token": token,
80
+ "client_id": self.config.openid_client_id,
81
+ "client_secret": self.config.openid_client_secret,
82
+ }
83
+
84
+ try:
85
+ response = requests.post(self.config.introspect_url, data=payloads, headers=headers)
86
+ if response.status_code != 200:
87
+ raise Eb_AuthError(response.status_code)
88
+
89
+ token_info = response.json()
90
+ if not token_info.get("active"):
91
+ raise Eb_AuthExpired(str(token_info))
92
+
93
+ return UserId(**token_info)
94
+
95
+ except requests.RequestException as e:
96
+ raise Eb_OI_InternalError(e)
97
+
98
+ def verify_basic(self, user: str, password: str) -> UserId:
99
+ raise Eb_AuthError(-1)
100
+
101
+ Eb_Auth.addAuth(r'auth::openid', Eb_AuthOpenID)
@@ -0,0 +1,140 @@
1
+ from json import loads as jsloads
2
+ from re import compile as re_compile
3
+ from pydantic import BaseModel
4
+ from ebuffer.models_common import UserCred, UserId, MessageLogin
5
+ from ebuffer.config import EbConfig, logger
6
+ from ebuffer.errors import Eb_Exception
7
+ from ebuffer.auth import Eb_Auth, Eb_AuthError, Eb_AuthExpired
8
+ from base64 import b64encode, b64decode
9
+ from os import urandom
10
+ from hashlib import sha256 as digest_method
11
+ import hmac
12
+
13
+ class Eb_AuthFile_LoginError(Eb_Exception):
14
+ def __init__(self, details: str = r''):
15
+ super().__init__(401, "AuthFileLoginError", "Could not login with local credentials.", details)
16
+
17
+ class Eb_AuthFile_InternalError(Eb_Exception):
18
+ def __init__(self, exc : Exception = None):
19
+ super().__init__(401, "AuthFileInternalError", "File based password error.", r'Invalid configuration', exc)
20
+
21
+ PassFileLoginExceptionList = \
22
+ Eb_AuthFile_LoginError().model()
23
+
24
+ PassFileAuthExceptionList = \
25
+ Eb_AuthError().model() | \
26
+ Eb_AuthExpired().model()
27
+
28
+ class EbConfigPassFile(BaseModel):
29
+ header_pass_through : bool = False
30
+ header_check_kw: str = r'shib-authentication-method'
31
+ header_check_val: str = r'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'
32
+ header_uid_kw: str = r'mail'
33
+ header_name_kw: str = r'displayname'
34
+ server_key: str = b'example'
35
+ user_list : str = [] # Json
36
+
37
+ class Eb_AutPassFile(Eb_Auth):
38
+ g_reEmail = re_compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
39
+
40
+ def __init__(self, config: EbConfig, name : str):
41
+ config = config.open_section(name, EbConfigPassFile)
42
+ super().__init__(config, name)
43
+ self.passdb = {}
44
+ self.bearerdb = {}
45
+ self.session_key : bytes = self._generate_hmac_key()
46
+ try:
47
+ user_list = config.user_list if isinstance(config.user_list, list) else jsloads(config.user_list)
48
+ for (user, pasw, info) in user_list:
49
+ self._add_user(user, pasw, info)
50
+ except Exception as exc:
51
+ raise Eb_AuthFile_InternalError(exc)
52
+
53
+ def _generate_hmac_key(self, length=256) -> bytes:
54
+ return self.config.server_key if self.config.server_key else urandom(length)
55
+
56
+ def _get_msgkey(self, user: str) -> bytes:
57
+ return f'Ebuffer for {user}'.encode('utf-8')
58
+
59
+ def _sign_hmac(self, user: str) -> str:
60
+ mac = hmac.new(self.session_key, msg=self._get_msgkey(user), digestmod=digest_method)
61
+ return b64encode(mac.digest()).decode('utf-8')
62
+
63
+ def _verify_hmac(self, user: str, signature_b64: str) -> bool:
64
+ mac = hmac.new(self.session_key, msg=self._get_msgkey(user), digestmod=digest_method)
65
+ signature = b64decode(signature_b64)
66
+ return hmac.compare_digest(mac.digest(), signature)
67
+
68
+ def _add_user(self, user: str , pasw: str, info: dict):
69
+ if r'email' not in info: raise RuntimeError(r'Missing email')
70
+ email = info[r'email']
71
+ if not self.g_reEmail.match(email): raise RuntimeError(r'Invalid email')
72
+ if r'name' not in info: info[r'name'] = user
73
+
74
+ if not pasw:
75
+ pasw = self._sign_hmac(user)
76
+
77
+ bearer = b64encode(f"{user}:{pasw}".encode()).decode()
78
+ self.passdb[user] = (pasw, info)
79
+ logger.info(f'New user: {user}: {pasw} [{info}]' )
80
+
81
+ info[r'access_token'] = bearer
82
+ self.bearerdb[bearer] = info
83
+
84
+ def login(self, user: UserCred | None, headers: dict) -> MessageLogin:
85
+ userInfo = None
86
+ if self.config.header_pass_through:
87
+ check = self.config.header_check_kw
88
+ content = self.config.header_check_val
89
+ check = check.strip().lower()
90
+ content = content.strip().lower()
91
+ activated, h_uid, h_name = False, None, None
92
+ for h, v in headers.items():
93
+ hs = h.strip().lower()
94
+ vs = v.strip().lower()
95
+ if hs == check and content == vs: activated = True
96
+ if hs == self.config.header_uid_kw: h_uid = v
97
+ if hs == self.config.header_name_kw: h_name = v
98
+ if activated:
99
+ if not h_uid or not h_name: raise Eb_AuthFile_LoginError(r'Missing data for SSO login.')
100
+ logger.debug(r"----- Header %s ------", str(h_uid))
101
+ if h_uid not in self.passdb:
102
+ self._add_user(h_uid, None, {r'name': h_name, r'email' : f'{user}@aqmo.org'})
103
+ password, userInfo = self.passdb[h_uid]
104
+ return MessageLogin(access_token=userInfo['access_token'])
105
+
106
+ if user:
107
+ logger.debug(r"----- PassFile %s ------", str(user))
108
+ name = user.username
109
+ if name not in self.passdb: raise Eb_AuthFile_LoginError(r'User %s not found.' % name)
110
+ password, userInfo = self.passdb[name]
111
+ if user.password != password: raise Eb_AuthFile_LoginError(r'Invalid password.')
112
+ else:
113
+ raise Eb_AuthFile_LoginError(r'Missing credential data for login.')
114
+
115
+ return MessageLogin(access_token=userInfo['access_token'])
116
+
117
+ def verify_token(self, scheme: str, token: str) -> UserId:
118
+ if token in self.bearerdb: return UserId(**self.bearerdb[token])
119
+ elif token.find(r':') == 1:
120
+ (user, pasw) = token.split(r':')
121
+ if self._verify_hmac(self, user, pasw):
122
+ logger.warning(r"----- lost user recovered %s ------", str(user))
123
+ if user not in self.passdb:
124
+ self._add_user(user, None, {r'name': r'<lost>', r'email' : f'{user}@aqmo.org'})
125
+ return UserId(**self.bearerdb[token])
126
+ else:
127
+ raise Eb_AuthFile_LoginError(r' Unauthorized token.')
128
+ else: raise Eb_AuthFile_LoginError(r'Invalid token.')
129
+
130
+ def verify_basic(self, user: str, password: str) -> UserId:
131
+ token = b64encode(f"{user}:{password}".encode()).decode()
132
+ if token not in self.bearerdb: raise Eb_AuthError(-1)
133
+ return UserId(**self.bearerdb[token])
134
+
135
+ def getRealmHeader(self) -> dict:
136
+ return {
137
+ "WWW-Authenticate": 'Basic realm="Access to the secure endpoint", charset="UTF-8", Bearer'
138
+ }
139
+
140
+ Eb_Auth.addAuth(r'auth::passfile', Eb_AutPassFile)
@@ -0,0 +1,15 @@
1
+ from ebuffer.backends.backends import Eb_DataBackend, Eb_Data
2
+
3
+ from ebuffer.backends.backends import Eb_BackendDuplicatedError, Eb_BackendMissingError
4
+ from ebuffer.backends.backends import Eb_BackendMissingBufferError, Eb_BackendWriteError, Eb_BackendReadError
5
+ from ebuffer.backends.memory import Eb_BackendMemoryMaxSizeError
6
+ from ebuffer.backends.mmap import Eb_BackendFileMaxSizeError
7
+
8
+ DataAllocExceptionList = {}
9
+ DataWriteExceptionList = \
10
+ Eb_BackendMissingBufferError().model() | \
11
+ Eb_BackendMemoryMaxSizeError().model() | \
12
+ Eb_BackendFileMaxSizeError().model()
13
+
14
+ DataReadExceptionList = \
15
+ Eb_BackendMissingBufferError().model()
@@ -0,0 +1,35 @@
1
+ # https://gridcf.org/gct-docs/
2
+ # https://pylibacl.k1024.org/index.html
3
+ # https://github.com/gridcf/gct
4
+
5
+
6
+ from typing import Optional
7
+ from pydantic import BaseModel
8
+
9
+ class ACL_V(BaseModel):
10
+ rwx : bytes[3]
11
+ def __str__(self): return r'%c%c%c' % (self.rwx[0],self.rwx[1],self.rwx[2])
12
+
13
+ class ACL_U(BaseModel):
14
+ ug: bool
15
+ name: str
16
+ value: ACL_V
17
+ def __str__(self): return r'%s:%s:%s' % (r'user' if self.ug else r'group', self.name, self.value)
18
+
19
+ class ACL(BaseModel):
20
+ user: str
21
+ group: str
22
+ vuser: ACL_V
23
+ vgroup: ACL_V
24
+ vother: ACL_V
25
+ vmask: ACL_V
26
+ aclist: [ ACL_U ]
27
+ vattr: dict
28
+ def __str__(self): return r'%s:%s:%s' % (r'user' if self.ug else r'group', self.name, self.value)
29
+
30
+ class Eb_Data(BaseModel):
31
+ path: str
32
+
33
+ def open(self): pass
34
+ def write(self): pass
35
+ def close(self): pass