Files
controller-wisetopdataserver/python/biopro/file/user/manager.py
T
2021-12-20 14:52:55 +08:00

356 lines
10 KiB
Python

import re
from pathlib import Path
from shutil import rmtree
from typing import Iterable, List, Optional, Dict, Union
from biopro.recording import RecordingMetaFile
from biopro.text import *
from biopro.util.address import ADDRESS, is_address_type, str_address
from biopro.util.console import YELLOW
from biopro.util.lock import *
from biopro.util.logger import LoggerFlag, logging_verbose
from biopro.util.validator import para_validator_build
from .alias import UserDeviceAlias
from .api import UserSettingAPI, UserSettingOptions
from .password import PasswordManager
from .setting import UserSetting, AbstractUserSetting
from .setup import UserSetup
_missing = object()
USER_NAME_PATTERN = re.compile(r'[a-zA-Z0-9_ @%().\-+\[\]]+')
def valid_user(f=None, *, user: str = 'user', allow_null=False):
"""user validating decorator"""
def _validator(self: 'UserSettingManager', u: Optional[str]):
if u is None:
if allow_null:
return
else:
raise RuntimeError(USER_NOT_FOUND)
if not self._validate_user(u):
raise RuntimeError(USER_NOT_FOUND, u)
build = para_validator_build(user, _validator)
if f is not None:
return build(f)
else:
return build
def valid_user_password(f=None, *, user: str = 'user', password: str = 'password'):
"""user password validating decorator"""
def _validator(self: 'UserSettingManager', u: str, w: str):
if not self._validate_user_password(u, w):
raise RuntimeError(PASSWORD_NOT_CORRECT, u)
build = para_validator_build((user, password), _validator)
if f is not None:
return build(f)
else:
return build
class UserSettingManager(LoggerFlag, UserSettingAPI, Synchronized):
"""user setting manager"""
PASSWORD_FILE = '.passwd'
__slots__ = '__root', '__user', '__password'
def __init__(self, options: UserSettingOptions):
LoggerFlag.__init__(self, 'UserSetting', YELLOW)
Synchronized.__init__(self)
path = Path(options.user_repository)
if options.debug_account_system:
p = PasswordManager(path / self.PASSWORD_FILE)
else:
# TODO re-check file path
p = PasswordManager(Path('/var/BPS') / self.PASSWORD_FILE)
self.__password = p
self.__root = path
self.__user: Dict[str, AbstractUserSetting] = {}
@synchronized
@logging_verbose
def shutdown(self):
for u in self.__user.values():
if u.save():
self.log_verbose('wrote', u.user)
@property
def user_repository(self) -> Path:
return self.__root
@logging_verbose
def user_list(self) -> List[str]:
return self.__password.user_list()
@logging_verbose
def user_info(self, user: str) -> AbstractUserSetting:
return self._user_info(user)
@synchronized
def _user_info(self, user: str) -> AbstractUserSetting:
# check user existed or not.
self.__password.user_hash(user)
if not self.__root.exists():
self.__root.mkdir(parents=True)
try:
# get from cache
u = self.__user[user]
except KeyError:
self.log_verbose('load', 'user', user)
# load from file
u = UserSetting.load(self.__root, user)
self.__user[user] = u
return u
@synchronized
@logging_verbose
def user_add(self, user: str, password: str) -> bool:
if not USER_NAME_PATTERN.match(user):
raise RuntimeError(ILLEGAL_USER_NAME, user)
self.__password.user_add(user, password)
# invalid cache
try:
del self.__user[user]
except KeyError:
pass
# clear previous setting if existed
p = self.__root / user
if p.exists():
rmtree(str(p), ignore_errors=True)
# new setting
self._user_info(user)
return True
def user_check_password_strength(self, password: str) -> Optional[str]:
return self.__password.check_password_strength(password)
def user_change_password(self, user: str, password: str, new_password: str):
self.log_verbose('user_change_password', user)
self.__password.user_change(user, password, new_password)
@synchronized
def user_del(self, user: str, password: str) -> bool:
self.log_verbose('user_del', user)
self.__password.user_del(user, password)
# invalid cache
try:
del self.__user[user]
except KeyError:
pass
# clear previous setting if existed
p = self.__root / user
if p.exists():
rmtree(str(p), ignore_errors=True)
return True
def user_validate(self, user: str, password: str) -> bool:
self.log_verbose('user_validate', user)
return self._validate_user_password(user, password)
def _validate_user(self, user: str) -> bool:
# TODO cool-down
return user in self.__password.user_list()
def _validate_user_password(self, user: str, password: str) -> bool:
# TODO cool-down
return self.__password.user_verify(user, password)
@synchronized
@logging_verbose
def user_sync(self, user: str):
try:
if self.__user[user].save():
self.log_verbose('wrote', user)
except KeyError:
# not in cache, setting not loaded, pass it
pass
@valid_user
@logging_verbose
def configuration_list(self, user: str) -> List[str]:
ret = []
for p in (self.__root / user).iterdir():
if p.is_file() and p.name.endswith(RecordingMetaFile.FILE_EXT):
ret.append(p.stem)
return ret
def _foreach_configuration(self, user: str) -> Iterable[RecordingMetaFile]:
for p in (self.__root / user).iterdir():
if p.is_file() and p.name.endswith(RecordingMetaFile.FILE_EXT):
try:
yield RecordingMetaFile.load(p)
except:
self.log_warn('load fail', str(p))
@valid_user
@logging_verbose
def configuration_filter(self, user: str, *, library: str = None, device: str = None) -> List[str]:
ret = []
for meta in self._foreach_configuration(user):
if library is not None:
if library not in meta.configuration.library:
continue
# if device is not None:
# if device not in meta.device.device_name:
# continue
ret.append(meta.filename)
return ret
@valid_user
@logging_verbose
def configuration_get(self, user: str, name: str) -> RecordingMetaFile:
if '/' in name:
raise ValueError('illegal configuration name : ' + name)
return RecordingMetaFile.load(self.__root / user / (name + RecordingMetaFile.FILE_EXT))
@valid_user
@logging_verbose
def configuration_add(self, user: str, name: str, meta: Union[str, Path, RecordingMetaFile]):
if '/' in name:
raise ValueError('illegal configuration name : ' + name)
if isinstance(meta, str):
if not meta.endswith(RecordingMetaFile.FILE_EXT):
meta = meta + RecordingMetaFile.FILE_EXT
meta = Path(meta)
if isinstance(meta, Path):
meta = RecordingMetaFile.load(meta)
if not isinstance(meta, RecordingMetaFile):
raise TypeError('not a meta file : ' + str(meta))
p = self.__root / user
c = meta.clone(p / (name + RecordingMetaFile.FILE_EXT))
c.clear_recording_file()
c.write()
@valid_user
@logging_verbose
def configuration_del(self, user: str, name: str) -> bool:
if '/' in name:
raise ValueError('illegal configuration name : ' + name)
p = self.__root / user
m = p / (name + RecordingMetaFile.FILE_EXT)
if m.exists():
m.unlink()
return True
else:
return False
@valid_user(user='user')
@valid_user(user='from_user', allow_null=True)
@logging_verbose
def configuration_clone(self, user: str, name: str, new_name: str, from_user: Optional[str] = None) -> bool:
if '/' in name:
raise ValueError('illegal configuration name : ' + name)
if '/' in new_name:
raise ValueError('illegal configuration name : ' + new_name)
s = self.__root / (from_user if from_user is not None else user)
t = self.__root / user
sf = s / (name + RecordingMetaFile.FILE_EXT)
tf = t / (new_name + RecordingMetaFile.FILE_EXT)
if not sf.exists():
return False
m = RecordingMetaFile.load(sf)
c = m.clone(tf)
c.clear_recording_file()
c.write()
return True
@logging_verbose
def setup_list(self, user: str) -> List[str]:
return self._user_info(user).setup_list()
@logging_verbose
def setup_info(self, user: str, name: str) -> UserSetup:
s = self._user_info(user).setup_info(name)
if s is None:
raise RuntimeError('setup not found')
return s
@logging_verbose
def setup_new(self, user: str, name: str) -> UserSetup:
return self._user_info(user).setup_new(name)
@logging_verbose
def setup_del(self, user: str, name: str) -> bool:
return self._user_info(user).setup_del(name)
@logging_verbose
def alias_list(self, user: str) -> List[UserDeviceAlias]:
s = self._user_info(user)
return [s.alias_info(a) for a in s.alias_list()]
@logging_verbose
def alias_info(self, user: str, name: Union[str, ADDRESS]) -> Optional[UserDeviceAlias]:
s = self._user_info(user)
if is_address_type(name):
return s.alias_info(name)
elif len(name) > 0:
for a in s.alias_list():
a = s.alias_info(a)
if a.device_alias == name or a.device_name == name:
return a
return None
@logging_verbose
def alias_new(self, user: str, address: Union[str, ADDRESS]) -> UserDeviceAlias:
if isinstance(address, str):
address = str_address(address)
return self._user_info(user).alias_new(address)
@logging_verbose
def alias_del(self, user: str, address: Union[str, ADDRESS]) -> bool:
if isinstance(address, str):
address = str_address(address)
return self._user_info(user).alias_del(address)