Skip to content
Snippets Groups Projects
Commit d7ccf5f3 authored by Martin Mareš's avatar Martin Mareš
Browse files

More bits...

parent 2b729ce7
No related branches found
No related tags found
No related merge requests found
......@@ -14,15 +14,15 @@ from typing import Union, Optional, Callable, Dict, List, Set
async def main():
bus = await MessageBus(negotiate_unix_fd=True).connect()
ins = await bus.introspect('cz.ucw.test', '/cz/ucw/Machine')
ins = await bus.introspect('cz.ucw.shipcat', '/cz/ucw/ShipCat')
pxy = bus.get_proxy_object('cz.ucw.test', '/cz/ucw/Machine', ins)
pif = pxy.get_interface('cz.ucw.TestInterface')
pxy = bus.get_proxy_object('cz.ucw.shipcat', '/cz/ucw/ShipCat', ins)
pif = pxy.get_interface('cz.ucw.ShipCat')
# fd = os.open('/dev/null', os.O_RDWR)
xx = await pif.call_shell(0, 1, 2)
# xx = await pif.call_shell(0, 1, 2)
# xx = await pif.call_frobate(42, 'brum')
xx = await pif.call_check('test')
print(xx)
......
......@@ -7,14 +7,16 @@ from dbus_next.service import ServiceInterface, method, dbus_property, signal
from dbus_next import Variant, DBusError, BusType, MessageType, RequestNameReply
import asyncio
import logging
import os
import sys
from typing import Union, Optional, Callable, Dict, List, Set, Any
from shipcat.config import ContainerConfig, ConfigError
from shipcat.config import GlobalConfig, ContainerConfig, ConfigError
class BusLogic:
config: GlobalConfig
bus: MessageBus
msg_tasks: Set[asyncio.Task]
dbus_iface: ProxyInterface
......@@ -23,8 +25,18 @@ class BusLogic:
method_dict: Dict[str, intr.Method]
interface_intro: intr.Interface
def __init__(self) -> None:
def __init__(self, config: GlobalConfig) -> None:
self.config = config
self.logger = logging.getLogger('shipscat')
if config.verbosity > 0:
self.logger.setLevel(logging.DEBUG)
else:
self.logger.setLevel(logging.INFO)
self.logger.propagate = True
self.msg_tasks = set()
self.object_tree = {
'cz': {
'ucw': {
......@@ -35,13 +47,9 @@ class BusLogic:
self.method_list = [
intr.Method(
name='Frobate',
name='Check',
in_args=[
intr.Arg('i', intr.ArgDirection.IN, 'foo'),
intr.Arg('s', intr.ArgDirection.IN, 'bar'),
],
out_args=[
intr.Arg('a{us}', intr.ArgDirection.OUT),
intr.Arg('s', intr.ArgDirection.IN, 'container'),
],
),
intr.Method(
......@@ -52,7 +60,7 @@ class BusLogic:
intr.Arg('h', intr.ArgDirection.IN, 'stderr_fd'),
],
out_args=[
intr.Arg('i', intr.ArgDirection.OUT),
intr.Arg('i', intr.ArgDirection.OUT, 'exit_code'),
],
),
]
......@@ -80,16 +88,7 @@ class BusLogic:
await self.bus.wait_for_disconnect()
def handle_message(self, msg: Message) -> Optional[Union[Message, bool]]:
print('=====================')
print('type', msg.message_type)
print('dest', msg.destination)
print('path', msg.path)
print('iface', msg.interface)
print('member', msg.member)
print('sender', msg.sender)
print('fds', msg.unix_fds)
print('serials', msg.serial, msg.reply_serial)
print('body', msg.body)
self.logger.debug(f'Got message: type={msg.message_type.name} dest={msg.destination} path={msg.path} iface={msg.interface} member={msg.member} sender={msg.sender} fds={msg.unix_fds}')
if (msg.destination == 'cz.ucw.shipcat'
and msg.interface == 'org.freedesktop.DBus.Introspectable'
......@@ -118,17 +117,16 @@ class BusLogic:
msg.unix_fds = []
def handle_method_call(self, msg: Message) -> bool:
print('Scheduling method call:', msg.member)
async def async_method_call(msg):
self.logger.debug(f'Calling method {msg.member}')
try:
reply = await getattr(self, 'handle_' + msg.member)(msg)
except DBusError as e:
print(f'DBusError: {e}')
self.logger.debug(f'DBusError: {e}')
reply = Message.new_error(msg, e.type, e.text)
except Exception as e:
print(f'Internal error: {e}')
reply = Message.new_error(msg, 'cz.ucw.Test.Error.Internal', 'Internal error')
self.logger.error(f'Internal error: {e}')
reply = Message.new_error(msg, 'cz.ucw.ShipCat.Error.Internal', 'Internal error')
self.bus.send(reply)
self.close_fds(msg)
......@@ -138,6 +136,7 @@ class BusLogic:
return True
def handle_introspect(self, msg: Message) -> Message:
self.logger.debug(f'Introspecting {msg.path}')
intro = intr.Node.default(msg.path)
# We do not want to speak ObjectManager-ish
intro.interfaces = [i for i in intro.interfaces if i.name != 'org.freedesktop.DBus.ObjectManager']
......@@ -153,42 +152,83 @@ class BusLogic:
while i < len(path) and isinstance(node, dict) and path[i] in node:
node = node[path[i]]
i += 1
print(path, i, node)
if i == len(path):
if isinstance(node, dict):
intro.nodes = [intr.Node(n) for n in sorted(node.keys())]
else:
intro.interfaces.append(self.interface_intro)
print('INTRO', intro.tostring())
return Message.new_method_return(msg, signature='s', body=[intro.tostring()])
async def handle_Frobate(self, msg: Message) -> Message:
print('Inside handle_Frobate:', msg)
cred = await self.dbus_iface.call_get_connection_credentials(msg.sender)
print('Creds:', cred)
return Message.new_method_return(
msg,
signature='a{us}',
body=[{1: 'one', 2: 'two'}],
)
async def handle_Check(self, msg: Message) -> Message:
[container] = msg.body
await self.setup_container(container, msg)
return Message.new_method_return(msg)
async def handle_Shell(self, msg: Message) -> Message:
stdin_fd = msg.unix_fds[msg.body[0]]
stdout_fd = msg.unix_fds[msg.body[1]]
stderr_fd = msg.unix_fds[msg.body[2]]
print('FDs:', stdin_fd, stdout_fd, stderr_fd)
cred = await self.dbus_iface.call_get_connection_credentials(msg.sender)
print('Creds:', cred)
proc = await asyncio.create_subprocess_exec('/bin/cat', '/etc/profile', stdin=stdin_fd, stdout=stdout_fd, stderr=stderr_fd)
await proc.wait()
return Message.new_method_return(msg, signature='i', body=[proc.returncode])
async def setup_container(self, name: str, msg: Message) -> ContainerConfig:
cred = await self.dbus_iface.call_get_connection_credentials(msg.sender)
assert isinstance(cred, dict)
unix_user_id = cred.get('UnixUserID', None)
if isinstance(unix_user_id, Variant) and unix_user_id.signature == 'u':
uid = unix_user_id.value
assert isinstance(uid, int)
else:
uid = -1
unix_group_ids = cred.get('UnixGroupIDs', None)
if isinstance(unix_group_ids, Variant) and unix_group_ids.signature == 'a(u)':
gids = unix_user_id.value
else:
gids = []
try:
cc = ContainerConfig.load(self.config, name)
except ConfigError as e:
self.logger.error(f'{msg.member}({name}) by uid {uid}: {e}')
raise DBusError('cz.ucw.ShipCat.Error.ContainerConfig', 'Cannot load container configuration')
if not self.check_rights(cc, uid, gids):
self.logger.error(f'{msg.member}({name}) by uid {uid}: Permission denied')
raise DBusError('cz.ucw.ShipCat.Error.PermissionDenied', 'You do not have permission to handle this container')
self.logger.info(f'{msg.member}({name}) by uid {uid}')
return cc
def check_rights(self, cc: ContainerConfig, uid: int, gids: List[int]) -> bool:
if uid == 0:
return True
if uid in cc.allowed_users:
return True
for gid in gids:
assert isinstance(gid, int)
if gid in cc.allowed_groups:
return True
return False
try:
config = GlobalConfig.load('shipcat.toml')
except ConfigError as e:
print(e, file=sys.stderr)
sys.exit(1)
bl = BusLogic()
logging.basicConfig(format='%(asctime)-15s %(levelname)-5.5s %(message)s')
bl = BusLogic(config)
asyncio.get_event_loop().run_until_complete(bl.loop())
......@@ -3,8 +3,10 @@
from pwd import getpwnam
from grp import getgrnam
from pathlib import Path
import re
import tomllib
from typing import List
from typing import Set
from .json_walker import Walker, WalkerError
......@@ -13,27 +15,61 @@ class ConfigError(RuntimeError):
pass
class GlobalConfig:
container_dir: Path
verbosity: int
@classmethod
def load(self, filename: str) -> 'GlobalConfig':
try:
with open(filename, 'rb') as f:
root = tomllib.load(f)
except FileNotFoundError:
raise ConfigError(f'Cannot load {filename}')
except tomllib.TOMLDecodeError as e:
raise ConfigError(f'Cannot parse {filename}: {e}')
gc = GlobalConfig()
try:
gc.parse(Walker(root))
except WalkerError as e:
raise ConfigError(f'Cannot parse {filename}: {e}')
return gc
def parse(self, walker: Walker) -> None:
with walker.enter_object() as w:
self.container_dir = Path(w['container_dir'].as_str())
self.verbosity = w['verbosity'].as_int(0)
class ContainerConfig:
container: str
root_dir: str
image: str
allowed_users: List[int]
allowed_groups: List[int]
allowed_users: Set[int]
allowed_groups: Set[int]
@classmethod
def load(cls, filename: str) -> 'ContainerConfig':
def load(cls, global_config: GlobalConfig, name: str) -> 'ContainerConfig':
if not re.fullmatch(r'[0-9A-Za-z_-]+', name):
raise ConfigError(f'Invalid container name {name}')
path = global_config.container_dir / (name + '.toml')
try:
with open(filename, 'rb') as f:
with open(path, 'rb') as f:
root = tomllib.load(f)
except FileNotFoundError:
raise ConfigError('Cannot open {filename}')
raise ConfigError(f'Cannot load {path}')
except tomllib.TOMLDecodeError as e:
raise ConfigError(f'Cannot parse {filename}: {e}')
raise ConfigError(f'Cannot parse {path}: {e}')
cc = ContainerConfig()
cc.container = name
try:
cc.parse(Walker(root))
except WalkerError as e:
raise ConfigError(f'Cannot parse {filename}: {e}')
raise ConfigError(f'Cannot parse {path}: {e}')
return cc
......@@ -42,20 +78,20 @@ class ContainerConfig:
self.root_dir = w['root_dir'].as_str()
self.image = w['image'].as_str()
self.allowed_users = []
self.allowed_users = set()
for wu in w['allowed_users'].default_to([]).array_values():
name = wu.as_str()
try:
pwd = getpwnam(name)
except KeyError:
wu.raise_error(f'Unknown user {name}')
self.allowed_users.append(pwd.pw_uid)
self.allowed_users.add(pwd.pw_uid)
self.allowed_groups = []
self.allowed_groups = set()
for wu in w['allowed_groups'].default_to([]).array_values():
name = wu.as_str()
try:
grp = getgrnam(name)
except KeyError:
wu.raise_error(f'Unknown group {name}')
self.allowed_groups.append(grp.gr_gid)
self.allowed_groups.add(grp.gr_gid)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment