mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-12 01:00:29 +08:00
unix socket auth
This commit is contained in:
parent
16a1dbd9ed
commit
7e185d2ad9
@ -360,6 +360,11 @@ def _get_config_scheme() -> dict:
|
||||
"enabled": Option(True, type=valid_bool),
|
||||
"expire": Option(0, type=valid_expire),
|
||||
|
||||
"usc": {
|
||||
"users": Option([], 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
|
||||
},
|
||||
|
||||
"internal": {
|
||||
"type": Option("htpasswd"),
|
||||
"force_users": Option([], type=valid_users_list),
|
||||
|
||||
@ -77,6 +77,8 @@ def main(argv: (list[str] | None)=None) -> None:
|
||||
auth_manager=AuthManager(
|
||||
enabled=config.auth.enabled,
|
||||
expire=config.auth.expire,
|
||||
usc_users=config.auth.usc.users,
|
||||
usc_groups=config.auth.usc.groups,
|
||||
unauth_paths=([] if config.prometheus.auth.enabled else ["/export/prometheus/metrics"]),
|
||||
|
||||
int_type=config.auth.internal.type,
|
||||
|
||||
@ -31,6 +31,7 @@ from ....htserver import HttpExposed
|
||||
from ....htserver import exposed_http
|
||||
from ....htserver import make_json_response
|
||||
from ....htserver import set_request_auth_info
|
||||
from ....htserver import get_request_unix_credentials
|
||||
|
||||
from ....validators.auth import valid_user
|
||||
from ....validators.auth import valid_passwd
|
||||
@ -76,6 +77,14 @@ async def check_request_auth(auth_manager: AuthManager, exposed: HttpExposed, re
|
||||
raise ForbiddenError()
|
||||
return
|
||||
|
||||
if exposed.allow_usc:
|
||||
creds = get_request_unix_credentials(req)
|
||||
if creds is not None:
|
||||
user = auth_manager.check_unix_credentials(creds) # type: ignore
|
||||
if user:
|
||||
set_request_auth_info(req, f"{user}[{creds.uid}] (unix)")
|
||||
return
|
||||
|
||||
raise UnauthorizedError()
|
||||
|
||||
|
||||
@ -85,7 +94,7 @@ class AuthApi:
|
||||
|
||||
# =====
|
||||
|
||||
@exposed_http("POST", "/auth/login", auth_required=False)
|
||||
@exposed_http("POST", "/auth/login", auth_required=False, allow_usc=False)
|
||||
async def __login_handler(self, req: Request) -> Response:
|
||||
if self.__auth_manager.is_auth_enabled():
|
||||
credentials = await req.post()
|
||||
@ -99,13 +108,14 @@ class AuthApi:
|
||||
raise ForbiddenError()
|
||||
return make_json_response()
|
||||
|
||||
@exposed_http("POST", "/auth/logout")
|
||||
@exposed_http("POST", "/auth/logout", allow_usc=False)
|
||||
async def __logout_handler(self, req: Request) -> Response:
|
||||
if self.__auth_manager.is_auth_enabled():
|
||||
token = valid_auth_token(req.cookies.get(_COOKIE_AUTH_TOKEN, ""))
|
||||
self.__auth_manager.logout(token)
|
||||
return make_json_response()
|
||||
|
||||
@exposed_http("GET", "/auth/check")
|
||||
# XXX: This handle is used for access control so it should NEVER allow access by socket credentials
|
||||
@exposed_http("GET", "/auth/check", allow_usc=False)
|
||||
async def __check_handler(self, _: Request) -> Response:
|
||||
return make_json_response()
|
||||
|
||||
@ -20,6 +20,8 @@
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import pwd
|
||||
import grp
|
||||
import dataclasses
|
||||
import time
|
||||
import datetime
|
||||
@ -35,6 +37,7 @@ from ...plugins.auth import BaseAuthService
|
||||
from ...plugins.auth import get_auth_service_class
|
||||
|
||||
from ...htserver import HttpExposed
|
||||
from ...htserver import RequestUnixCredentials
|
||||
|
||||
|
||||
# =====
|
||||
@ -49,11 +52,13 @@ class _Session:
|
||||
assert self.expire_ts >= 0
|
||||
|
||||
|
||||
class AuthManager: # pylint: disable=too-many-instance-attributes
|
||||
class AuthManager: # pylint: disable=too-many-arguments,too-many-instance-attributes
|
||||
def __init__(
|
||||
self,
|
||||
enabled: bool,
|
||||
expire: int,
|
||||
usc_users: list[str],
|
||||
usc_groups: list[str],
|
||||
unauth_paths: list[str],
|
||||
|
||||
int_type: str,
|
||||
@ -78,9 +83,15 @@ class AuthManager: # pylint: disable=too-many-instance-attributes
|
||||
logger.info("Maximum user session time is limited: %s",
|
||||
self.__format_seconds(expire))
|
||||
|
||||
self.__usc_uids = self.__load_usc_uids(usc_users, usc_groups)
|
||||
if self.__usc_uids:
|
||||
logger.info("Unauth UNIX socket access is allowed for users: %s",
|
||||
list(self.__usc_uids.values()))
|
||||
|
||||
self.__unauth_paths = frozenset(unauth_paths) # To speed up
|
||||
for path in self.__unauth_paths:
|
||||
logger.warning("Authorization is disabled for API %r", path)
|
||||
if self.__unauth_paths:
|
||||
logger.info("Authorization is disabled for APIs: %s",
|
||||
list(self.__unauth_paths))
|
||||
|
||||
self.__int_service: (BaseAuthService | None) = None
|
||||
if enabled:
|
||||
@ -244,3 +255,29 @@ class AuthManager: # pylint: disable=too-many-instance-attributes
|
||||
await self.__int_service.cleanup()
|
||||
if self.__ext_service:
|
||||
await self.__ext_service.cleanup()
|
||||
|
||||
# =====
|
||||
|
||||
def __load_usc_uids(self, users: list[str], groups: list[str]) -> dict[int, str]:
|
||||
uids: dict[int, str] = {}
|
||||
|
||||
pwds: dict[str, int] = {}
|
||||
for pw in pwd.getpwall():
|
||||
assert pw.pw_name == pw.pw_name.strip()
|
||||
assert pw.pw_name
|
||||
pwds[pw.pw_name] = pw.pw_uid
|
||||
if pw.pw_name in users:
|
||||
uids[pw.pw_uid] = pw.pw_name
|
||||
|
||||
for gr in grp.getgrall():
|
||||
if gr.gr_name in groups:
|
||||
for member in gr.gr_mem:
|
||||
if member in pwds:
|
||||
uid = pwds[member]
|
||||
uids[uid] = member
|
||||
|
||||
return uids
|
||||
|
||||
def check_unix_credentials(self, creds: RequestUnixCredentials) -> (str | None):
|
||||
assert self.__enabled
|
||||
return self.__usc_uids.get(creds.uid)
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import asyncio
|
||||
import contextlib
|
||||
import dataclasses
|
||||
@ -83,6 +84,7 @@ class HttpExposed:
|
||||
method: str
|
||||
path: str
|
||||
auth_required: bool
|
||||
allow_usc: bool
|
||||
handler: Callable
|
||||
|
||||
|
||||
@ -90,14 +92,22 @@ _HTTP_EXPOSED = "_http_exposed"
|
||||
_HTTP_METHOD = "_http_method"
|
||||
_HTTP_PATH = "_http_path"
|
||||
_HTTP_AUTH_REQUIRED = "_http_auth_required"
|
||||
_HTTP_ALLOW_USC = "_http_allow_usc"
|
||||
|
||||
|
||||
def exposed_http(http_method: str, path: str, auth_required: bool=True) -> Callable:
|
||||
def exposed_http(
|
||||
http_method: str,
|
||||
path: str,
|
||||
auth_required: bool=True,
|
||||
allow_usc: bool=True,
|
||||
) -> Callable:
|
||||
|
||||
def set_attrs(handler: Callable) -> Callable:
|
||||
setattr(handler, _HTTP_EXPOSED, True)
|
||||
setattr(handler, _HTTP_METHOD, http_method)
|
||||
setattr(handler, _HTTP_PATH, path)
|
||||
setattr(handler, _HTTP_AUTH_REQUIRED, auth_required)
|
||||
setattr(handler, _HTTP_ALLOW_USC, allow_usc)
|
||||
return handler
|
||||
return set_attrs
|
||||
|
||||
@ -108,6 +118,7 @@ def _get_exposed_http(obj: object) -> list[HttpExposed]:
|
||||
method=getattr(handler, _HTTP_METHOD),
|
||||
path=getattr(handler, _HTTP_PATH),
|
||||
auth_required=getattr(handler, _HTTP_AUTH_REQUIRED),
|
||||
allow_usc=getattr(handler, _HTTP_ALLOW_USC),
|
||||
handler=handler,
|
||||
)
|
||||
for handler in [getattr(obj, name) for name in dir(obj)]
|
||||
@ -270,6 +281,34 @@ def set_request_auth_info(req: BaseRequest, info: str) -> None:
|
||||
setattr(req, _REQUEST_AUTH_INFO, info)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class RequestUnixCredentials:
|
||||
pid: int
|
||||
uid: int
|
||||
gid: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
assert self.pid > 0
|
||||
assert self.uid >= 0
|
||||
assert self.gid >= 0
|
||||
|
||||
|
||||
def get_request_unix_credentials(req: BaseRequest) -> (RequestUnixCredentials | None):
|
||||
if req.transport is None:
|
||||
return None
|
||||
sock = req.transport.get_extra_info("socket")
|
||||
if sock is None:
|
||||
return None
|
||||
try:
|
||||
data = sock.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize("iii"))
|
||||
except Exception:
|
||||
return None
|
||||
(pid, uid, gid) = struct.unpack("iii", data)
|
||||
if pid <= 0 or uid < 0 or gid < 0:
|
||||
return None
|
||||
return RequestUnixCredentials(pid=pid, uid=uid, gid=gid)
|
||||
|
||||
|
||||
# =====
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class WsSession:
|
||||
@ -314,13 +353,14 @@ class HttpServer:
|
||||
|
||||
if unix_rm and os.path.exists(unix_path):
|
||||
os.remove(unix_path)
|
||||
server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
server_socket.bind(unix_path)
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1)
|
||||
sock.bind(unix_path)
|
||||
if unix_mode:
|
||||
os.chmod(unix_path, unix_mode)
|
||||
|
||||
run_app(
|
||||
sock=server_socket,
|
||||
sock=sock,
|
||||
app=self.__make_app(),
|
||||
shutdown_timeout=1,
|
||||
access_log_format=access_log_format,
|
||||
|
||||
@ -83,3 +83,6 @@ StorageContext.read_port_names
|
||||
StorageContext.read_atx_cp_delays
|
||||
StorageContext.read_atx_cpl_delays
|
||||
StorageContext.read_atx_cr_delays
|
||||
|
||||
RequestUnixCredentials.pid
|
||||
RequestUnixCredentials.gid
|
||||
|
||||
@ -40,9 +40,9 @@ from kvmd.crypto import KvmdHtpasswdFile
|
||||
|
||||
|
||||
# =====
|
||||
_E_AUTH = HttpExposed("GET", "/foo_auth", True, (lambda: None))
|
||||
_E_UNAUTH = HttpExposed("GET", "/bar_unauth", True, (lambda: None))
|
||||
_E_FREE = HttpExposed("GET", "/baz_free", False, (lambda: None))
|
||||
_E_AUTH = HttpExposed("GET", "/foo_auth", auth_required=True, allow_usc=True, handler=(lambda: None))
|
||||
_E_UNAUTH = HttpExposed("GET", "/bar_unauth", auth_required=True, allow_usc=True, handler=(lambda: None))
|
||||
_E_FREE = HttpExposed("GET", "/baz_free", auth_required=False, allow_usc=True, handler=(lambda: None))
|
||||
|
||||
|
||||
def _make_service_kwargs(path: str) -> dict:
|
||||
@ -62,6 +62,8 @@ async def _get_configured_manager(
|
||||
manager = AuthManager(
|
||||
enabled=True,
|
||||
expire=0,
|
||||
usc_users=[],
|
||||
usc_groups=[],
|
||||
unauth_paths=unauth_paths,
|
||||
|
||||
int_type="htpasswd",
|
||||
@ -262,6 +264,8 @@ async def test_ok__disabled() -> None:
|
||||
manager = AuthManager(
|
||||
enabled=False,
|
||||
expire=0,
|
||||
usc_users=[],
|
||||
usc_groups=[],
|
||||
unauth_paths=[],
|
||||
|
||||
int_type="foobar",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user