import asyncio
import locale
import logging
import os
import pwd
from typing import Dict, List, Sequence, Tuple
from cockpit._vendor.ferny import AskpassHandler
from cockpit._vendor.systemd_ctypes import Variant, bus
HELPER_SOCKET = '/run/polkit/agent-helper.socket'
HELPER_PATH = '/usr/lib/polkit-1/polkit-agent-helper-1'
AGENT_DBUS_PATH = '/PolkitAgent'
logger = logging.getLogger(__name__)
Identity = Tuple[str, Dict[str, Variant]]
class org_freedesktop_PolicyKit1_AuthenticationAgent(bus.Object):
def __init__(self, responder: AskpassHandler):
super().__init__()
self.responder = responder
@bus.Interface.Method('', ['s', 's', 's', 'a{ss}', 's', 'a(sa{sv})'])
async def begin_authentication(self, action_id: str, message: str, icon_name: str,
details: Dict[str, str], cookie: str, identities: Sequence[Identity]) -> None:
logger.debug('BeginAuthentication: action %s, message "%s", icon %s, details %s, cookie %s, identities %r',
action_id, message, icon_name, details, cookie, identities)
my_uid = os.geteuid()
for (auth_type, subject) in identities:
if auth_type == 'unix-user' and 'uid' in subject and subject['uid'].value == my_uid:
logger.debug('Authentication subject %s matches our uid %d', subject, my_uid)
break
else:
logger.warning('Not supporting authentication as any of %s', identities)
return
user_name = pwd.getpwuid(my_uid).pw_name
try:
await self._authenticate_socket(user_name, cookie)
except OSError:
logger.debug('Socket helper not available, falling back to legacy helper')
await self._authenticate_suid_helper(user_name, cookie)
async def _authenticate_suid_helper(self, user_name: str, cookie: str) -> None:
"""Authenticate using legacy setuid helper binary"""
logger.debug('Trying legacy polkit helper at %s', HELPER_PATH)
process = await asyncio.create_subprocess_exec(HELPER_PATH, user_name, cookie,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE)
assert process.stdin is not None
assert process.stdout is not None
try:
await self._communicate(process.stdin, process.stdout)
except asyncio.CancelledError:
logger.debug('Cancelled authentication')
finally:
try:
process.terminate()
except ProcessLookupError:
pass
res = await process.wait()
logger.debug('helper exited with code %i', res)
async def _authenticate_socket(self, user_name: str, cookie: str) -> None:
"""Authenticate using socket-activated helper (polkit >= 127)"""
logger.debug('Trying socket-activated polkit helper at %s', HELPER_SOCKET)
reader, writer = await asyncio.open_unix_connection(HELPER_SOCKET)
try:
writer.write(f'{user_name}\n{cookie}\n'.encode())
await writer.drain()
await self._communicate(writer, reader)
except asyncio.CancelledError:
logger.debug('Cancelled authentication')
finally:
writer.close()
await writer.wait_closed()
logger.debug('socket connection closed')
async def _communicate(self, stdin: asyncio.StreamWriter, stdout: asyncio.StreamReader) -> None:
messages: List[str] = []
async for line in stdout:
logger.debug('Read line from helper: %s', line)
command, _, value = line.strip().decode().partition(' ')
if command.startswith('PAM_PROMPT'):
if value.startswith('Password'):
value = ''
passwd = await self.responder.do_askpass('\n'.join(messages), value, '')
messages.clear()
if passwd is None:
logger.debug('got PAM_PROMPT %s, but do_askpass returned None', value)
raise asyncio.CancelledError('no password given')
logger.debug('got PAM_PROMPT %s, do_askpass returned a password', value)
stdin.write(passwd.encode())
stdin.write(b'\n')
del passwd
await stdin.drain()
logger.debug('got PAM_PROMPT, wrote password to helper')
elif command in ('PAM_TEXT_INFO', 'PAM_ERROR'):
messages.append(value)
elif command == 'SUCCESS':
logger.debug('Authentication succeeded')
break
elif command == 'FAILURE':
logger.warning('Authentication failed')
break
else:
logger.warning('Unknown line from helper, aborting: %s', line)
break
class PolkitAgent:
"""Register polkit agent when required
Use this as a context manager to ensure that the agent gets unregistered again.
"""
def __init__(self, responder: AskpassHandler):
self.responder = responder
self.agent_slot = None
async def __aenter__(self):
try:
self.system_bus = bus.Bus.default_system()
except OSError as e:
logger.warning('cannot connect to system bus, not registering polkit agent: %s', e)
return self
try:
self.subject = ('unix-session', {'session-id': Variant(os.environ['XDG_SESSION_ID'], 's')})
except KeyError:
logger.debug('XDG_SESSION_ID not set, not registering polkit agent')
return self
agent_object = org_freedesktop_PolicyKit1_AuthenticationAgent(self.responder)
self.agent_slot = self.system_bus.add_object(AGENT_DBUS_PATH, agent_object)
locale_name = locale.setlocale(locale.LC_MESSAGES, None)
await self.system_bus.call_method_async(
'org.freedesktop.PolicyKit1',
'/org/freedesktop/PolicyKit1/Authority',
'org.freedesktop.PolicyKit1.Authority',
'RegisterAuthenticationAgent',
'(sa{sv})ss',
self.subject, locale_name, AGENT_DBUS_PATH)
logger.debug('Registered agent for %r and locale %s', self.subject, locale_name)
return self
async def __aexit__(self, _exc_type, _exc_value, _traceback):
if self.agent_slot:
await self.system_bus.call_method_async(
'org.freedesktop.PolicyKit1',
'/org/freedesktop/PolicyKit1/Authority',
'org.freedesktop.PolicyKit1.Authority',
'UnregisterAuthenticationAgent',
'(sa{sv})s',
self.subject, AGENT_DBUS_PATH)
self.agent_slot.cancel()
logger.debug('Unregistered agent for %r', self.subject)