mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-14 22:45:47 +00:00
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:
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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:
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user