Files
Ansible/scripts/addons/webgui/tests/test_executor_unix.py
T
2026-09-22 19:23:17 +02:00

76 lines
3.9 KiB
Python

"""Linux local socket/UID integration. Synthetic core inventory, no remote hosts."""
from dataclasses import replace
from pathlib import Path
import grp
import json
import os
import pwd
import shutil
import signal
import socket
import struct
import subprocess
import sys
import tempfile
import time
import pytest
from aim_webgui.config import Settings
from aim_webgui.core.client import CoreClient
from aim_webgui.credentials import wire
def test_nonroot_executor_public_core_and_peer_rejection(core_root):
if os.geteuid()!=0 or not hasattr(socket,'SO_PEERCRED'):
pytest.skip('Needs Linux root in the isolated test container to exercise different local UIDs.')
account=pwd.getpwnam('nobody')
base=Path(tempfile.mkdtemp(prefix='aim-exec-'));base.chmod(0o755)
process=None
try:
core=base/'core';shutil.copytree(core_root,core)
for p in [core,*core.rglob('*')]:
if p.is_dir():p.chmod(0o755)
else:p.chmod(0o644)
config=core/'scripts/aim.yml'
config.write_text(f'root_dir: {core}\nservice_user: synthetic\nrequired_group: {grp.getgrgid(account.pw_gid).gr_name}\n')
runtime=base/'socket';runtime.mkdir(mode=0o711);os.chown(runtime,account.pw_uid,account.pw_gid)
home=base/'home';home.mkdir(mode=0o700);os.chown(home,account.pw_uid,account.pw_gid)
cfg=base/'web.toml'
cfg.write_text('[core]\ntransport="unix"\n'+f'command=[{json.dumps(sys.executable)},{json.dumps(str(core/"scripts/aimctl.py"))}]\n'+
f'config={json.dumps(str(config))}\nhome={json.dumps(str(home))}\nsocket={json.dumps(str(runtime/"core.sock"))}\nexecutor_user="nobody"\nclient_user="root"\n')
cfg.chmod(0o644)
settings=Settings.load(cfg)
source=Path(__file__).resolve().parents[1]/'src'
def drop():os.setgroups([0,account.pw_gid]);os.setgid(account.pw_gid);os.setuid(account.pw_uid)
process=subprocess.Popen([sys.executable,'-B','-m','aim_webgui','--config',str(cfg),'executor'],
env={**os.environ,'PYTHONPATH':str(source)},stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,preexec_fn=drop)
for _ in range(200):
if settings.core_socket.exists():break
if process.poll()is not None:pytest.fail('Executor fixture exited before opening socket')
time.sleep(.05)
assert settings.core_socket.exists()
assert settings.core_socket.stat().st_uid==account.pw_uid
assert runtime.stat().st_gid==account.pw_gid
assert settings.core_socket.stat().st_gid==0
assert settings.core_socket.stat().st_mode&0o777==0o660
client=CoreClient(settings)
assert client.request('capabilities')['core_version']=='3.3.0rc8'
assert client.request('list_customers')==['example']
# A direct local client cannot smuggle an execution executable/path field.
with socket.socket(socket.AF_UNIX)as conn:
conn.connect(str(settings.core_socket));wire.send(conn,{'request':{'api_version':'1.0','operation':'list_customers','command':'id'},'credential_frame':False,'start_seconds':None})
assert wire.receive(conn)['type']=='transport_error'
# Make only this synthetic endpoint reachable to a different UID, so the
# SO_PEERCRED check, rather than DAC alone, is exercised.
runtime.chmod(0o755);settings.core_socket.chmod(0o666)
script='''import socket,sys
s=socket.socket(socket.AF_UNIX);s.connect(sys.argv[1]);s.settimeout(3)
assert s.recv(4)==b''
'''
other=pwd.getpwnam('daemon')
def other_drop():os.setgroups([]);os.setgid(other.pw_gid);os.setuid(other.pw_uid)
result=subprocess.run([sys.executable,'-c',script,str(settings.core_socket)],preexec_fn=other_drop,capture_output=True,timeout=5)
assert result.returncode==0
finally:
if process and process.poll()is None:process.send_signal(signal.SIGTERM);process.wait(timeout=20)
shutil.rmtree(base)