openwebrx-clone/owrx/users.py

238 lines
6.9 KiB
Python
Raw Normal View History

2020-04-01 20:29:42 +00:00
from abc import ABC, abstractmethod
2021-02-11 18:31:44 +00:00
from owrx.config.core import CoreConfig
from datetime import datetime, timezone
2020-04-01 20:29:42 +00:00
import json
2021-02-06 17:38:49 +00:00
import hashlib
2021-02-06 17:43:37 +00:00
import os
2021-02-18 20:07:45 +00:00
import stat
2020-04-01 20:29:42 +00:00
import logging
logger = logging.getLogger(__name__)
class PasswordException(Exception):
pass
class Password(ABC):
@staticmethod
def from_dict(d: dict):
if "encoding" not in d:
raise PasswordException("password encoding not set")
if d["encoding"] == "string":
return CleartextPassword(d)
2021-02-06 17:38:49 +00:00
elif d["encoding"] == "hash":
return HashedPassword(d)
2020-04-01 20:29:42 +00:00
raise PasswordException("invalid passord encoding: {0}".format(d["type"]))
@abstractmethod
2021-02-06 17:38:49 +00:00
def is_valid(self, inp: str) -> bool:
2020-04-01 20:29:42 +00:00
pass
2021-02-06 17:04:32 +00:00
@abstractmethod
2021-02-06 17:38:49 +00:00
def toJson(self) -> dict:
2021-02-06 17:04:32 +00:00
pass
2020-04-01 20:29:42 +00:00
class CleartextPassword(Password):
2021-02-06 17:04:32 +00:00
def __init__(self, pwinfo):
if isinstance(pwinfo, str):
self._value = pwinfo
elif isinstance(pwinfo, dict):
self._value = pwinfo["value"]
else:
raise ValueError("invalid argument to ClearTextPassword()")
2021-02-06 17:38:49 +00:00
def is_valid(self, inp: str) -> bool:
2021-02-06 17:04:32 +00:00
return self._value == inp
2021-02-06 17:38:49 +00:00
def toJson(self) -> dict:
2021-02-06 17:04:32 +00:00
return {
"encoding": "string",
"value": self._value
}
2020-04-01 20:29:42 +00:00
2021-02-06 17:38:49 +00:00
class HashedPassword(Password):
def __init__(self, pwinfo, algorithm="sha256"):
self.iterations = 100000
if isinstance(pwinfo, str):
2021-02-06 17:38:49 +00:00
self._createFromString(pwinfo, algorithm)
else:
self._loadFromDict(pwinfo)
def _createFromString(self, pw: str, algorithm: str):
self._algorithm = algorithm
2021-02-06 17:43:37 +00:00
self._salt = os.urandom(32)
dk = hashlib.pbkdf2_hmac(self._algorithm, pw.encode(), self._salt, self.iterations)
2021-02-06 17:38:49 +00:00
self._hash = dk.hex()
pass
def _loadFromDict(self, d: dict):
self._hash = d["value"]
self._algorithm = d["algorithm"]
2021-02-06 17:43:37 +00:00
self._salt = bytes.fromhex(d["salt"])
2021-02-06 17:38:49 +00:00
pass
def is_valid(self, inp: str) -> bool:
2021-02-06 17:43:37 +00:00
dk = hashlib.pbkdf2_hmac(self._algorithm, inp.encode(), self._salt, self.iterations)
2021-02-06 17:38:49 +00:00
return dk.hex() == self._hash
def toJson(self) -> dict:
return {
"encoding": "hash",
"value": self._hash,
"algorithm": self._algorithm,
2021-02-06 17:43:37 +00:00
"salt": self._salt.hex(),
2021-02-06 17:38:49 +00:00
}
DefaultPasswordClass = HashedPassword
2021-02-06 17:22:13 +00:00
2020-04-01 20:29:42 +00:00
class User(object):
def __init__(self, name: str, enabled: bool, password: Password, must_change_password: bool = False):
2020-04-01 20:29:42 +00:00
self.name = name
self.enabled = enabled
self.password = password
self.must_change_password = must_change_password
2020-04-01 20:29:42 +00:00
2021-02-06 17:04:32 +00:00
def toJson(self):
return {
"user": self.name,
"enabled": self.enabled,
"must_change_password": self.must_change_password,
2021-02-06 17:04:32 +00:00
"password": self.password.toJson()
}
@staticmethod
def fromJson(d):
if "user" in d and "password" in d and "enabled" in d:
mcp = d["must_change_password"] if "must_change_password" in d else False
return User(d["user"], d["enabled"], Password.from_dict(d["password"]), mcp)
def setPassword(self, password: Password, must_change_password: bool = None):
2021-02-06 18:12:44 +00:00
self.password = password
if must_change_password is not None:
self.must_change_password = must_change_password
2021-02-06 18:12:44 +00:00
2021-02-08 16:04:55 +00:00
def is_enabled(self):
return self.enabled
def enable(self):
self.enabled = True
def disable(self):
self.enabled = False
2020-04-01 20:29:42 +00:00
class UserList(object):
sharedInstance = None
@staticmethod
def getSharedInstance():
if UserList.sharedInstance is None:
UserList.sharedInstance = UserList()
return UserList.sharedInstance
def __init__(self):
self.file_modified = None
self.users = {}
def refresh(self):
if self.file_modified is None or self._getUsersFileModifiedTimestamp() > self.file_modified:
logger.debug("reloading users from disk due to file modification")
self.users = self._loadUsers()
2020-04-01 20:29:42 +00:00
2021-02-06 17:04:32 +00:00
def _getUsersFile(self):
config = CoreConfig()
return "{data_directory}/users.json".format(data_directory=config.get_data_directory())
def _getUsersFileModifiedTimestamp(self):
2021-02-12 16:00:35 +00:00
timestamp = 0
try:
timestamp = os.path.getmtime(self._getUsersFile())
except FileNotFoundError:
pass
return datetime.fromtimestamp(timestamp, timezone.utc)
2020-04-01 20:29:42 +00:00
def _loadUsers(self):
2021-02-06 17:04:32 +00:00
usersFile = self._getUsersFile()
# to avoid concurrency issues and problems when parsing errors occur:
# get early, store late
modified = self._getUsersFileModifiedTimestamp()
2021-02-06 17:04:32 +00:00
try:
with open(usersFile, "r") as f:
2020-04-01 20:29:42 +00:00
users_json = json.load(f)
2021-02-06 17:04:32 +00:00
users = {u.name: u for u in [User.fromJson(d) for d in users_json]}
self.file_modified = modified
return users
2021-02-06 17:04:32 +00:00
except FileNotFoundError:
2021-02-12 16:00:35 +00:00
self.file_modified = modified
2021-02-06 17:04:32 +00:00
return {}
except json.JSONDecodeError:
logger.exception("error while parsing users file %s", usersFile)
return {}
except Exception:
logger.exception("error while processing users from %s", usersFile)
return {}
def _userToJson(self, u):
return u.toJson()
2021-02-06 18:12:44 +00:00
def store(self):
2021-02-06 17:04:32 +00:00
usersFile = self._getUsersFile()
users = [u.toJson() for u in self.values()]
2021-02-06 17:04:32 +00:00
try:
# don't write directly to file to avoid corruption on exceptions
jsonContent = json.dumps(users, indent=4)
2021-02-06 17:04:32 +00:00
with open(usersFile, "w") as f:
f.write(jsonContent)
2021-02-18 20:07:45 +00:00
# file should be readable by us only
os.chmod(usersFile, stat.S_IWUSR + stat.S_IRUSR)
2021-02-06 17:04:32 +00:00
except Exception:
logger.exception("error while writing users file %s", usersFile)
self.refresh()
2021-02-06 17:04:32 +00:00
2021-02-06 17:57:51 +00:00
def _getUsername(self, user):
if isinstance(user, User):
return user.name
elif isinstance(user, str):
return user
else:
raise ValueError("invalid user type")
2021-02-06 17:04:32 +00:00
def addUser(self, user: User):
self[user.name] = user
2021-02-06 17:15:02 +00:00
def deleteUser(self, user):
2021-02-06 17:57:51 +00:00
del self[self._getUsername(user)]
2021-02-06 17:15:02 +00:00
def __delitem__(self, key):
self.refresh()
2021-02-06 17:15:02 +00:00
if key not in self.users:
raise KeyError("User {user} doesn't exist".format(user=key))
del self.users[key]
2021-02-06 18:12:44 +00:00
self.store()
2021-02-06 17:15:02 +00:00
2020-04-01 20:29:42 +00:00
def __getitem__(self, item):
self.refresh()
2020-04-01 20:29:42 +00:00
return self.users[item]
def __contains__(self, item):
self.refresh()
2020-04-01 20:29:42 +00:00
return item in self.users
2021-02-06 17:04:32 +00:00
def __setitem__(self, key, value):
self.refresh()
2021-02-06 17:04:32 +00:00
if key in self.users:
raise KeyError("User {user} already exists".format(user=key))
self.users[key] = value
2021-02-06 18:12:44 +00:00
self.store()
2021-02-08 16:04:55 +00:00
def values(self):
self.refresh()
2021-02-08 16:04:55 +00:00
return self.users.values()