implement support of unix socket for server

This feature can be used for unauthorized access to apis - e.g. for
reporting service if it is run on the same machine. Since now it becomes
recommended way for the interprocess communication, thus some options
(e.g. creating user with as-service flag) are no longer available now
This commit is contained in:
2022-11-29 01:18:01 +02:00
parent 4811dec759
commit 0161617e36
24 changed files with 247 additions and 134 deletions

View File

@ -640,6 +640,7 @@ def _set_repo_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--sign-target", help="sign options", action="append",
type=SignSettings.from_option, choices=enum_values(SignSettings))
parser.add_argument("--web-port", help="port of the web service", type=int)
parser.add_argument("--web-unix-socket", help="path to unix socket used for interprocess communications", type=Path)
parser.set_defaults(handler=handlers.Setup, lock=None, report=False, quiet=True, unsafe=True)
return parser
@ -782,12 +783,10 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("user-add", help="create or update user",
description="update user for web services with the given password and role. "
"In case if password was not entered it will be asked interactively",
epilog="In case of first run (i.e. if password salt is not set yet) or if ``as-service`` "
"flag is supplied, this action requires root privileges because it performs write "
"to filesystem configuration.",
epilog="In case of first run (i.e. if password salt is not set yet) this action requires "
"root privileges because it performs write to filesystem configuration.",
formatter_class=_formatter)
parser.add_argument("username", help="username for web service")
parser.add_argument("--as-service", help="add user as service user", action="store_true")
parser.add_argument("-p", "--password", help="user password. Blank password will be treated as empty password, "
"which is in particular must be used for OAuth2 authorization type.")
parser.add_argument("-r", "--role", help="user access level",

View File

@ -118,6 +118,10 @@ class Setup(Handler):
section = Configuration.section_name("web", architecture)
configuration.set_option(section, "port", str(args.web_port))
if args.web_unix_socket is not None:
section = Configuration.section_name("web", architecture)
configuration.set_option(section, "unix_socket", str(args.web_unix_socket))
target = include_path / "00-setup-overrides.ini"
with target.open("w") as ahriman_configuration:
configuration.write(ahriman_configuration)

View File

@ -58,9 +58,9 @@ class Users(Handler):
old_salt, salt = Users.get_salt(configuration)
user = Users.user_create(args)
if old_salt is None or args.as_service:
if old_salt is None:
auth_configuration = Users.configuration_get(configuration.include)
Users.configuration_create(auth_configuration, user, salt, args.as_service, args.secure)
Users.configuration_create(auth_configuration, salt, args.secure)
database.user_update(user.hash_password(salt))
elif args.action == Action.List:
@ -72,22 +72,16 @@ class Users(Handler):
database.user_remove(args.username)
@staticmethod
def configuration_create(configuration: Configuration, user: User, salt: str,
as_service_user: bool, secure: bool) -> None:
def configuration_create(configuration: Configuration, salt: str, secure: bool) -> None:
"""
enable configuration if it has been disabled
Args:
configuration(Configuration): configuration instance
user(User): user descriptor
salt(str): password hash salt
as_service_user(bool): add user as service user, also set password and user to configuration
secure(bool): if true then set file permissions to 0o600
"""
configuration.set_option("auth", "salt", salt)
if as_service_user:
configuration.set_option("web", "username", user.username)
configuration.set_option("web", "password", user.password)
Users.configuration_write(configuration, secure)
@staticmethod

View File

@ -52,8 +52,12 @@ class Client:
address = configuration.get("web", "address", fallback=None)
host = configuration.get("web", "host", fallback=None)
port = configuration.getint("web", "port", fallback=None)
socket = configuration.get("web", "unix_socket", fallback=None)
if address or (host and port):
# basically we just check if there is something we can use for interaction with remote server
# at the moment (end of 2022) I think it would be much better idea to introduce flag like `enabled`,
# but it will totally break used experience
if address or (host and port) or socket:
from ahriman.core.status.web_client import WebClient
return WebClient(configuration)
return cls()

View File

@ -21,6 +21,7 @@ import logging
import requests
from typing import List, Optional, Tuple
from urllib.parse import quote_plus as urlencode
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
@ -48,13 +49,12 @@ class WebClient(Client, LazyLogging):
Args:
configuration(Configuration): configuration instance
"""
self.address = self.parse_address(configuration)
self.address, use_unix_socket = self.parse_address(configuration)
self.user = User.from_option(
configuration.get("web", "username", fallback=None),
configuration.get("web", "password", fallback=None))
self.__session = requests.session()
self._login()
self.__session = self._create_session(use_unix_socket=use_unix_socket)
@property
def _login_url(self) -> str:
@ -77,7 +77,7 @@ class WebClient(Client, LazyLogging):
return f"{self.address}/api/v1/status"
@staticmethod
def parse_address(configuration: Configuration) -> str:
def parse_address(configuration: Configuration) -> Tuple[str, bool]:
"""
parse address from configuration
@ -85,15 +85,38 @@ class WebClient(Client, LazyLogging):
configuration(Configuration): configuration instance
Returns:
str: valid http address
Tuple[str, bool]: tuple of server address and socket flag (True in case if unix socket must be used)
"""
if (unix_socket := configuration.get("web", "unix_socket", fallback=None)) is not None:
# special pseudo-protocol which is used for unix sockets
return f"http+unix://{urlencode(unix_socket)}", True
address = configuration.get("web", "address", fallback=None)
if not address:
# build address from host and port directly
host = configuration.get("web", "host")
port = configuration.getint("web", "port")
address = f"http://{host}:{port}"
return address
return address, False
def _create_session(self, *, use_unix_socket: bool) -> requests.Session:
"""
generate new request session
Args:
use_unix_socket(bool): if set to True then unix socket session will be generated instead of native requests
Returns:
requests.Session: generated session object
"""
if use_unix_socket:
import requests_unixsocket # type: ignore
session: requests.Session = requests_unixsocket.Session()
return session
session = requests.Session()
self._login()
return session
def _login(self) -> None:
"""

View File

@ -19,6 +19,7 @@
#
import aiohttp_security # type: ignore
import base64
import socket
import types
from aiohttp import web
@ -101,7 +102,11 @@ def auth_handler(allow_read_only: bool) -> MiddlewareType:
"""
@middleware
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
if (permission_method := getattr(handler, "get_permission", None)) is not None:
if (unix_socket := request.get_extra_info("socket")) is not None and unix_socket.family == socket.AF_UNIX:
# special case for unix sockets. We need to extract socket which is used for the request
# and check its address family
permission = UserAccess.Unauthorized
elif (permission_method := getattr(handler, "get_permission", None)) is not None:
permission = await permission_method(request)
elif isinstance(handler, types.MethodType): # additional wrapper for static resources
handler_instance = getattr(handler, "__self__", None)

View File

@ -78,8 +78,9 @@ def run_server(application: web.Application) -> None:
configuration: Configuration = application["configuration"]
host = configuration.get("web", "host")
port = configuration.getint("web", "port")
unix_socket = configuration.get("web", "unix_socket", fallback=None)
web.run_app(application, host=host, port=port, handle_signals=False,
web.run_app(application, host=host, port=port, path=unix_socket, handle_signals=False,
access_log=logging.getLogger("http"), access_log_class=FilteredAccessLogger)