356 lines
10 KiB
Python
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)
|