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 +1 -0
- ebuffer/apiebuffer.py +80 -0
- ebuffer/apiuserobj.py +127 -0
- ebuffer/application.py +45 -0
- ebuffer/auth/__init__.py +15 -0
- ebuffer/auth/auth.py +51 -0
- ebuffer/auth/openid.py +101 -0
- ebuffer/auth/passfile.py +140 -0
- ebuffer/backends/__init__.py +15 -0
- ebuffer/backends/acl.py +35 -0
- ebuffer/backends/backends.py +187 -0
- ebuffer/backends/memory.py +137 -0
- ebuffer/backends/mmap.py +230 -0
- ebuffer/config.py +48 -0
- ebuffer/database/__init__.py +17 -0
- ebuffer/database/cruduser.py +59 -0
- ebuffer/database/cruduserobj.py +162 -0
- ebuffer/database/database.py +54 -0
- ebuffer/database/privmodel_buffer.py +37 -0
- ebuffer/database/privmodel_user.py +39 -0
- ebuffer/database/privmodel_userobj.py +35 -0
- ebuffer/errors.py +88 -0
- ebuffer/models_buffer.py +30 -0
- ebuffer/models_common.py +123 -0
- ebuffer/routers/__init__.py +2 -0
- ebuffer/routers/routes_auth.py +47 -0
- ebuffer/routers/routes_buffer.py +206 -0
- ebuffer/server/__init__.py +1 -0
- ebuffer/server/main.py +207 -0
- ebuffer-0.1.5.dist-info/METADATA +82 -0
- ebuffer-0.1.5.dist-info/RECORD +34 -0
- ebuffer-0.1.5.dist-info/WHEEL +5 -0
- ebuffer-0.1.5.dist-info/licenses/LICENSE +21 -0
- ebuffer-0.1.5.dist-info/top_level.txt +1 -0
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()
|
ebuffer/auth/__init__.py
ADDED
|
@@ -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)
|
ebuffer/auth/passfile.py
ADDED
|
@@ -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()
|
ebuffer/backends/acl.py
ADDED
|
@@ -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
|