diff --git a/PKGBUILD b/PKGBUILD index 20f7018c..03154f40 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -68,7 +68,7 @@ depends=( python-dbus python-dbus-next python-pygments - python-pyghmi + "python-pyghmi>=1.6.0-2" python-pam python-pillow python-xlib diff --git a/configs/kvmd/ipmipasswd b/configs/kvmd/ipmipasswd index d95fdfe1..f358fa13 100644 --- a/configs/kvmd/ipmipasswd +++ b/configs/kvmd/ipmipasswd @@ -1,14 +1,11 @@ -# This file describes the credentials for IPMI users. The first pair separated by colon -# is the login and password with which the user can access to IPMI. The second pair -# is the name and password with which the user can access to KVMD API. The arrow is used -# as a separator and shows the direction of user registration in the system. +# This file describes the credentials for IPMI users in format "login:password", +# one per line. The passwords are NOT encrypted. # # WARNING! IPMI protocol is completely unsafe by design. In short, the authentication # process for IPMI 2.0 mandates that the server send a salted SHA1 or MD5 hash of the -# requested user's password to the client, prior to the client authenticating. Never use -# the same passwords for KVMD and IPMI users. This default configuration is shown here -# for example only. +# requested user's password to the client, prior to the client authenticating. # -# And even better not to use IPMI. Instead, you can directly use KVMD API via curl. +# NEVER use the same passwords for KVMD and IPMI users. +# This default configuration is shown here just for the example only. -admin:admin -> admin:admin +admin:admin diff --git a/kvmd/apps/__init__.py b/kvmd/apps/__init__.py index 91866717..86b92522 100644 --- a/kvmd/apps/__init__.py +++ b/kvmd/apps/__init__.py @@ -362,7 +362,9 @@ def _get_config_scheme() -> dict: "expire": Option(0, type=valid_expire), "usc": { - "users": Option([], type=valid_users_list), # PiKVM username has a same regex as a UNIX username + "users": Option([ + "kvmd-ipmi", + ], type=valid_users_list), # PiKVM username has a same regex as a UNIX username "groups": Option([], type=valid_users_list), # groupname has a same regex as a username }, diff --git a/kvmd/apps/ipmi/auth.py b/kvmd/apps/ipmi/auth.py index 71a13fe7..01d1ea0c 100644 --- a/kvmd/apps/ipmi/auth.py +++ b/kvmd/apps/ipmi/auth.py @@ -20,7 +20,13 @@ # ========================================================================== # -import dataclasses +import threading +import functools +import time + +from ...logging import get_logger + +from ... import tools # ===== @@ -29,60 +35,42 @@ class IpmiPasswdError(Exception): super().__init__(f"Syntax error at {path}:{lineno}: {msg}") -@dataclasses.dataclass(frozen=True) -class IpmiUserCredentials: - ipmi_user: str - ipmi_passwd: str - kvmd_user: str - kvmd_passwd: str - - class IpmiAuthManager: def __init__(self, path: str) -> None: self.__path = path - with open(path) as file: - self.__credentials = self.__parse_passwd_file(file.read().split("\n")) + self.__lock = threading.Lock() - def __contains__(self, ipmi_user: str) -> bool: - return (ipmi_user in self.__credentials) + def get(self, user: str) -> (str | None): + creds = self.__get_credentials(int(time.time())) + return creds.get(user) - def __getitem__(self, ipmi_user: str) -> str: - return self.__credentials[ipmi_user].ipmi_passwd + @functools.lru_cache(maxsize=1) + def __get_credentials(self, ts: int) -> dict[str, str]: + _ = ts + with self.__lock: + try: + return self.__read_credentials() + except Exception as ex: + get_logger().error("%s", tools.efmt(ex)) + return {} - def get_credentials(self, ipmi_user: str) -> IpmiUserCredentials: - return self.__credentials[ipmi_user] + def __read_credentials(self) -> dict[str, str]: + with open(self.__path) as file: + creds: dict[str, str] = {} + for (lineno, line) in tools.passwds_splitted(file.read()): + if " -> " in line: # Compatibility with old ipmipasswd file format + line = line.split(" -> ", 1)[0] - def __parse_passwd_file(self, lines: list[str]) -> dict[str, IpmiUserCredentials]: - credentials: dict[str, IpmiUserCredentials] = {} - for (lineno, line) in enumerate(lines): - if len(line.strip()) == 0 or line.lstrip().startswith("#"): - continue + if ":" not in line: + raise IpmiPasswdError(self.__path, lineno, "Missing ':' operator") - if " -> " not in line: - raise IpmiPasswdError(self.__path, lineno, "Missing ' -> ' operator") + (user, passwd) = line.split(":", 1) + user = user.strip() + if len(user) == 0: + raise IpmiPasswdError(self.__path, lineno, "Empty IPMI user") - (left, right) = map(str.lstrip, line.split(" -> ", 1)) - for (name, pair) in [("left", left), ("right", right)]: - if ":" not in pair: - raise IpmiPasswdError(self.__path, lineno, f"Missing ':' operator in {name} credentials") + if user in creds: + raise IpmiPasswdError(self.__path, lineno, f"Found duplicating user {user!r}") - (ipmi_user, ipmi_passwd) = left.split(":") - ipmi_user = ipmi_user.strip() - if len(ipmi_user) == 0: - raise IpmiPasswdError(self.__path, lineno, "Empty IPMI user (left)") - - (kvmd_user, kvmd_passwd) = right.split(":") - kvmd_user = kvmd_user.strip() - if len(kvmd_user) == 0: - raise IpmiPasswdError(self.__path, lineno, "Empty KVMD user (left)") - - if ipmi_user in credentials: - raise IpmiPasswdError(self.__path, lineno, f"Found duplicating user {ipmi_user!r} (left)") - - credentials[ipmi_user] = IpmiUserCredentials( - ipmi_user=ipmi_user, - ipmi_passwd=ipmi_passwd, - kvmd_user=kvmd_user, - kvmd_passwd=kvmd_passwd, - ) - return credentials + creds[user] = passwd + return creds diff --git a/kvmd/apps/ipmi/server.py b/kvmd/apps/ipmi/server.py index 2fc897f9..391dbdcc 100644 --- a/kvmd/apps/ipmi/server.py +++ b/kvmd/apps/ipmi/server.py @@ -70,7 +70,6 @@ class IpmiServer(BaseIpmiServer): # pylint: disable=too-many-instance-attribute super().__init__(authdata=auth_manager, address=host, port=port) - self.__auth_manager = auth_manager self.__kvmd = kvmd self.__host = host @@ -165,11 +164,10 @@ class IpmiServer(BaseIpmiServer): # pylint: disable=too-many-instance-attribute def __make_request(self, session: IpmiServerSession, name: str, func_path: str, **kwargs): # type: ignore async def runner(): # type: ignore logger = get_logger(0) - credentials = self.__auth_manager.get_credentials(session.username.decode()) - logger.info("[%s]: Performing request %s from user %r (IPMI) as %r (KVMD)", - session.sockaddr[0], name, credentials.ipmi_user, credentials.kvmd_user) + logger.info("[%s]: Performing request %s from IPMI user %r ...", + session.sockaddr[0], name, session.username.decode()) try: - async with self.__kvmd.make_session(credentials.kvmd_user, credentials.kvmd_passwd) as kvmd_session: + async with self.__kvmd.make_session() as kvmd_session: func = functools.reduce(getattr, func_path.split("."), kvmd_session) return (await func(**kwargs)) except (aiohttp.ClientError, asyncio.TimeoutError) as ex: diff --git a/kvmd/clients/kvmd.py b/kvmd/clients/kvmd.py index 5600c28f..7558af3c 100644 --- a/kvmd/clients/kvmd.py +++ b/kvmd/clients/kvmd.py @@ -217,8 +217,10 @@ class KvmdClientSession(BaseHttpClientSession): class KvmdClient(BaseHttpClient): def make_session(self, user: str="", passwd: str="") -> KvmdClientSession: - headers = { - "X-KVMD-User": user, - "X-KVMD-Passwd": passwd, - } + headers: (dict[str, str] | None) = None + if user: + headers = { + "X-KVMD-User": user, + "X-KVMD-Passwd": passwd, + } return KvmdClientSession(lambda: self._make_http_session(headers)) diff --git a/kvmd/tools.py b/kvmd/tools.py index 6dd7d2f9..8f82fbe5 100644 --- a/kvmd/tools.py +++ b/kvmd/tools.py @@ -27,6 +27,7 @@ import multiprocessing.queues import queue import shlex +from typing import Generator from typing import TypeVar @@ -81,3 +82,13 @@ def build_cmd(cmd: list[str], cmd_remove: list[str], cmd_append: list[str]) -> l *filter((lambda item: item not in cmd_remove), cmd[1:]), *cmd_append, ] + + +# ===== +def passwds_splitted(text: str) -> Generator[tuple[int, str]]: + for (lineno, line) in enumerate(text.split("\n")): + line = line.rstrip("\r") + ls = line.strip() + if len(ls) == 0 or ls.startswith("#"): + continue + yield (lineno, line) diff --git a/testenv/requirements.txt b/testenv/requirements.txt index 36d2407a..874cc41b 100644 --- a/testenv/requirements.txt +++ b/testenv/requirements.txt @@ -1,6 +1,6 @@ python-periphery pyserial-asyncio -pyghmi +git+https://opendev.org/x/pyghmi.git#33cff21882b6782c20b054e6e8adcf94b5e09561 spidev pyrad types-PyYAML diff --git a/testenv/v2-hdmi-rpi4.override.yaml b/testenv/v2-hdmi-rpi4.override.yaml index 2c6f4d23..90e51282 100644 --- a/testenv/v2-hdmi-rpi4.override.yaml +++ b/testenv/v2-hdmi-rpi4.override.yaml @@ -1,4 +1,8 @@ kvmd: + auth: + usc: + users: [root] + server: unix_mode: 0666 diff --git a/testenv/v2-hdmiusb-rpi4.override.yaml b/testenv/v2-hdmiusb-rpi4.override.yaml index 215de3f5..a088bb78 100644 --- a/testenv/v2-hdmiusb-rpi4.override.yaml +++ b/testenv/v2-hdmiusb-rpi4.override.yaml @@ -1,4 +1,8 @@ kvmd: + auth: + usc: + users: [root] + server: unix_mode: 0666