mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-14 02:00:32 +08:00
ipmi: usinc usc auth
This commit is contained in:
parent
79d4d99f37
commit
c8cf06ee8c
2
PKGBUILD
2
PKGBUILD
@ -68,7 +68,7 @@ depends=(
|
|||||||
python-dbus
|
python-dbus
|
||||||
python-dbus-next
|
python-dbus-next
|
||||||
python-pygments
|
python-pygments
|
||||||
python-pyghmi
|
"python-pyghmi>=1.6.0-2"
|
||||||
python-pam
|
python-pam
|
||||||
python-pillow
|
python-pillow
|
||||||
python-xlib
|
python-xlib
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
# This file describes the credentials for IPMI users. The first pair separated by colon
|
# This file describes the credentials for IPMI users in format "login:password",
|
||||||
# is the login and password with which the user can access to IPMI. The second pair
|
# one per line. The passwords are NOT encrypted.
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
# WARNING! IPMI protocol is completely unsafe by design. In short, the authentication
|
# 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
|
# 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
|
# requested user's password to the client, prior to the client authenticating.
|
||||||
# the same passwords for KVMD and IPMI users. This default configuration is shown here
|
|
||||||
# for example only.
|
|
||||||
#
|
#
|
||||||
# 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
|
||||||
|
|||||||
@ -362,7 +362,9 @@ def _get_config_scheme() -> dict:
|
|||||||
"expire": Option(0, type=valid_expire),
|
"expire": Option(0, type=valid_expire),
|
||||||
|
|
||||||
"usc": {
|
"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
|
"groups": Option([], type=valid_users_list), # groupname has a same regex as a username
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -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}")
|
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:
|
class IpmiAuthManager:
|
||||||
def __init__(self, path: str) -> None:
|
def __init__(self, path: str) -> None:
|
||||||
self.__path = path
|
self.__path = path
|
||||||
with open(path) as file:
|
self.__lock = threading.Lock()
|
||||||
self.__credentials = self.__parse_passwd_file(file.read().split("\n"))
|
|
||||||
|
|
||||||
def __contains__(self, ipmi_user: str) -> bool:
|
def get(self, user: str) -> (str | None):
|
||||||
return (ipmi_user in self.__credentials)
|
creds = self.__get_credentials(int(time.time()))
|
||||||
|
return creds.get(user)
|
||||||
|
|
||||||
def __getitem__(self, ipmi_user: str) -> str:
|
@functools.lru_cache(maxsize=1)
|
||||||
return self.__credentials[ipmi_user].ipmi_passwd
|
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:
|
def __read_credentials(self) -> dict[str, str]:
|
||||||
return self.__credentials[ipmi_user]
|
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]:
|
if ":" not in line:
|
||||||
credentials: dict[str, IpmiUserCredentials] = {}
|
raise IpmiPasswdError(self.__path, lineno, "Missing ':' operator")
|
||||||
for (lineno, line) in enumerate(lines):
|
|
||||||
if len(line.strip()) == 0 or line.lstrip().startswith("#"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if " -> " not in line:
|
(user, passwd) = line.split(":", 1)
|
||||||
raise IpmiPasswdError(self.__path, lineno, "Missing ' -> ' operator")
|
user = user.strip()
|
||||||
|
if len(user) == 0:
|
||||||
|
raise IpmiPasswdError(self.__path, lineno, "Empty IPMI user")
|
||||||
|
|
||||||
(left, right) = map(str.lstrip, line.split(" -> ", 1))
|
if user in creds:
|
||||||
for (name, pair) in [("left", left), ("right", right)]:
|
raise IpmiPasswdError(self.__path, lineno, f"Found duplicating user {user!r}")
|
||||||
if ":" not in pair:
|
|
||||||
raise IpmiPasswdError(self.__path, lineno, f"Missing ':' operator in {name} credentials")
|
|
||||||
|
|
||||||
(ipmi_user, ipmi_passwd) = left.split(":")
|
creds[user] = passwd
|
||||||
ipmi_user = ipmi_user.strip()
|
return creds
|
||||||
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
|
|
||||||
|
|||||||
@ -70,7 +70,6 @@ class IpmiServer(BaseIpmiServer): # pylint: disable=too-many-instance-attribute
|
|||||||
|
|
||||||
super().__init__(authdata=auth_manager, address=host, port=port)
|
super().__init__(authdata=auth_manager, address=host, port=port)
|
||||||
|
|
||||||
self.__auth_manager = auth_manager
|
|
||||||
self.__kvmd = kvmd
|
self.__kvmd = kvmd
|
||||||
|
|
||||||
self.__host = host
|
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
|
def __make_request(self, session: IpmiServerSession, name: str, func_path: str, **kwargs): # type: ignore
|
||||||
async def runner(): # type: ignore
|
async def runner(): # type: ignore
|
||||||
logger = get_logger(0)
|
logger = get_logger(0)
|
||||||
credentials = self.__auth_manager.get_credentials(session.username.decode())
|
logger.info("[%s]: Performing request %s from IPMI user %r ...",
|
||||||
logger.info("[%s]: Performing request %s from user %r (IPMI) as %r (KVMD)",
|
session.sockaddr[0], name, session.username.decode())
|
||||||
session.sockaddr[0], name, credentials.ipmi_user, credentials.kvmd_user)
|
|
||||||
try:
|
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)
|
func = functools.reduce(getattr, func_path.split("."), kvmd_session)
|
||||||
return (await func(**kwargs))
|
return (await func(**kwargs))
|
||||||
except (aiohttp.ClientError, asyncio.TimeoutError) as ex:
|
except (aiohttp.ClientError, asyncio.TimeoutError) as ex:
|
||||||
|
|||||||
@ -217,8 +217,10 @@ class KvmdClientSession(BaseHttpClientSession):
|
|||||||
|
|
||||||
class KvmdClient(BaseHttpClient):
|
class KvmdClient(BaseHttpClient):
|
||||||
def make_session(self, user: str="", passwd: str="") -> KvmdClientSession:
|
def make_session(self, user: str="", passwd: str="") -> KvmdClientSession:
|
||||||
headers = {
|
headers: (dict[str, str] | None) = None
|
||||||
"X-KVMD-User": user,
|
if user:
|
||||||
"X-KVMD-Passwd": passwd,
|
headers = {
|
||||||
}
|
"X-KVMD-User": user,
|
||||||
|
"X-KVMD-Passwd": passwd,
|
||||||
|
}
|
||||||
return KvmdClientSession(lambda: self._make_http_session(headers))
|
return KvmdClientSession(lambda: self._make_http_session(headers))
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import multiprocessing.queues
|
|||||||
import queue
|
import queue
|
||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
|
from typing import Generator
|
||||||
from typing import TypeVar
|
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:]),
|
*filter((lambda item: item not in cmd_remove), cmd[1:]),
|
||||||
*cmd_append,
|
*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)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
python-periphery
|
python-periphery
|
||||||
pyserial-asyncio
|
pyserial-asyncio
|
||||||
pyghmi
|
git+https://opendev.org/x/pyghmi.git#33cff21882b6782c20b054e6e8adcf94b5e09561
|
||||||
spidev
|
spidev
|
||||||
pyrad
|
pyrad
|
||||||
types-PyYAML
|
types-PyYAML
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
kvmd:
|
kvmd:
|
||||||
|
auth:
|
||||||
|
usc:
|
||||||
|
users: [root]
|
||||||
|
|
||||||
server:
|
server:
|
||||||
unix_mode: 0666
|
unix_mode: 0666
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
kvmd:
|
kvmd:
|
||||||
|
auth:
|
||||||
|
usc:
|
||||||
|
users: [root]
|
||||||
|
|
||||||
server:
|
server:
|
||||||
unix_mode: 0666
|
unix_mode: 0666
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user