mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-09-19 04:59:01 +00:00
actions: Call to a privileged server if it is available
- Instead of running the command using sudo. If the server is not reachable, run the privileged command using sudo. Tests: - Typical privileged calls are made to server as evidenced by the network emoji icon in the log. - Some actions such as creating gitweb repository or downloading a backup archive happen via sudo instead of privileged daemon. - When a call is made to privileged daemon the log message is show just like a sudo call. - If the daemon is not running and can't be started, the calls are made to sudo. - If the daemon is rejects connections, then calls are automatically made to sudo. - When cloning a gitweb repository, the operation is immediately returned and task runs in background. Other tasks as waited upon until they are finished. Introducing a sleep in privileged method leads to increased page load time. - When server sends non-JSON response, a decode error is printed and exception is raised. - When a typical privileged call is made, the return value as expected. - When a typical privileged call raises exception, a nice HTML exception is shown in the UI. stdout/stderr outputs are not shown. Error is also logged on the console as expected but without stdout/stderr. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: Joseph Nuthalapati <njoseph@riseup.net>
This commit is contained in:
parent
99c28b583f
commit
213d0330fd
@ -9,6 +9,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
@ -23,6 +24,8 @@ EXIT_PERM = 20
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
socket_path = '/run/freedombox/privileged.socket'
|
||||||
|
|
||||||
|
|
||||||
# An alias for 'str' to mark some strings as sensitive. Sensitive strings are
|
# An alias for 'str' to mark some strings as sensitive. Sensitive strings are
|
||||||
# not logged. Use 'type secret_str = str' when Python 3.11 support is no longer
|
# not logged. Use 'type secret_str = str' when Python 3.11 support is no longer
|
||||||
@ -70,10 +73,133 @@ def privileged(func):
|
|||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
module_name = _get_privileged_action_module_name(func)
|
module_name = _get_privileged_action_module_name(func)
|
||||||
action_name = func.__name__
|
action_name = func.__name__
|
||||||
|
return _run_privileged_method(func, module_name, action_name, args,
|
||||||
|
kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def _run_privileged_method(func, module_name, action_name, args, kwargs):
|
||||||
|
"""Execute a privileged method either using a server or sudo."""
|
||||||
|
try:
|
||||||
|
return _run_privileged_method_on_server(func, module_name, action_name,
|
||||||
|
list(args), dict(kwargs))
|
||||||
|
except (
|
||||||
|
NotImplementedError, # For raw_output and run_as_user flags
|
||||||
|
FileNotFoundError, # When the .socket file is not present
|
||||||
|
ConnectionRefusedError, # When is daemon not running
|
||||||
|
ConnectionResetError # When daemon fails permission check
|
||||||
|
):
|
||||||
return _run_privileged_method_as_process(func, module_name,
|
return _run_privileged_method_as_process(func, module_name,
|
||||||
action_name, args, kwargs)
|
action_name, args, kwargs)
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
def _read_from_server(client_socket: socket.socket) -> bytes:
|
||||||
|
"""Read everything from a socket and return the data."""
|
||||||
|
response = b''
|
||||||
|
while True:
|
||||||
|
chunk = client_socket.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
|
||||||
|
response += chunk
|
||||||
|
|
||||||
|
return json.loads(response)
|
||||||
|
|
||||||
|
|
||||||
|
def _request_to_server(request: dict) -> socket.socket:
|
||||||
|
"""Connect to the server and make a request."""
|
||||||
|
request_string = json.dumps(request)
|
||||||
|
client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
try:
|
||||||
|
client_socket.connect(socket_path)
|
||||||
|
client_socket.sendall(request_string.encode('utf-8'))
|
||||||
|
# Close the write end of the socket signaling an EOF and no more data
|
||||||
|
# will be sent.
|
||||||
|
client_socket.shutdown(socket.SHUT_WR)
|
||||||
|
except Exception:
|
||||||
|
client_socket.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
return client_socket
|
||||||
|
|
||||||
|
|
||||||
|
def _run_privileged_method_on_server(func, module_name, action_name, args,
|
||||||
|
kwargs):
|
||||||
|
"""Execute a privileged method using a server."""
|
||||||
|
run_as_user = kwargs.pop('_run_as_user', None)
|
||||||
|
run_in_background = kwargs.pop('_run_in_background', False)
|
||||||
|
raw_output = kwargs.pop('_raw_output', False)
|
||||||
|
log_error = kwargs.pop('_log_error', True)
|
||||||
|
|
||||||
|
if raw_output or run_as_user:
|
||||||
|
raise NotImplementedError('Not yet implemented')
|
||||||
|
|
||||||
|
_log_action(func, module_name, action_name, args, kwargs, run_as_user,
|
||||||
|
run_in_background, is_server=True)
|
||||||
|
|
||||||
|
request = {
|
||||||
|
'module': module_name,
|
||||||
|
'action': action_name,
|
||||||
|
'args': args,
|
||||||
|
'kwargs': kwargs
|
||||||
|
}
|
||||||
|
client_socket = _request_to_server(request)
|
||||||
|
|
||||||
|
args = (func, module_name, action_name, args, kwargs, log_error,
|
||||||
|
client_socket)
|
||||||
|
if not run_in_background:
|
||||||
|
return _wait_for_server_response(*args)
|
||||||
|
|
||||||
|
read_thread = threading.Thread(target=_wait_for_server_response, args=args)
|
||||||
|
read_thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_for_server_response(func, module_name, action_name, args, kwargs,
|
||||||
|
log_error, client_socket):
|
||||||
|
"""Wait for the server to respond and process the response."""
|
||||||
|
try:
|
||||||
|
return_value = _read_from_server(client_socket)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.error('Error decoding action return value %s..%s(*%s, **%s)',
|
||||||
|
module_name, action_name, args, kwargs)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
client_socket.close()
|
||||||
|
|
||||||
|
if return_value['result'] == 'success':
|
||||||
|
return return_value['return']
|
||||||
|
|
||||||
|
module = importlib.import_module(return_value['exception']['module'])
|
||||||
|
exception_class = getattr(module, return_value['exception']['name'])
|
||||||
|
exception = exception_class(*return_value['exception']['args'])
|
||||||
|
exception.stdout = b''
|
||||||
|
exception.stderr = b''
|
||||||
|
|
||||||
|
def _get_html_message():
|
||||||
|
"""Return an HTML format error that can be shown in messages."""
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
|
formatted_args = _format_args(func, args, kwargs)
|
||||||
|
exception_args, stdout, stderr, traceback = _format_error(
|
||||||
|
exception, return_value)
|
||||||
|
return format_html('Error running action: {}..{}({}): {}({})\n{}{}{}',
|
||||||
|
module_name, action_name, formatted_args,
|
||||||
|
return_value['exception']['name'], exception_args,
|
||||||
|
stdout, stderr, traceback)
|
||||||
|
|
||||||
|
exception.get_html_message = _get_html_message
|
||||||
|
|
||||||
|
if log_error:
|
||||||
|
formatted_args = _format_args(func, args, kwargs)
|
||||||
|
exception_args, stdout, stderr, traceback = _format_error(
|
||||||
|
exception, return_value)
|
||||||
|
logger.error('Error running action %s..%s(%s): %s(%s)\n'
|
||||||
|
'%s%s%s', module_name, action_name, formatted_args,
|
||||||
|
return_value['exception']['name'], exception_args, stdout,
|
||||||
|
stderr, traceback)
|
||||||
|
|
||||||
|
raise exception
|
||||||
|
|
||||||
|
|
||||||
def _run_privileged_method_as_process(func, module_name, action_name, args,
|
def _run_privileged_method_as_process(func, module_name, action_name, args,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user