fix(sysauth): hold BeginAuthentication reply until auth resolves
polkitd ends the authentication session the moment the BeginAuthentication D-Bus reply returns, so replying immediately aborted every attempt with 'No session for cookie'. The invocation is now held until respond() or cancel() resolves it, and cancelled sessions return org.freedesktop.PolicyKit1.Error.Cancelled. Also export the agent on the system bus (polkitd addresses agents by the system-bus unique name used for registration) and authenticate through polkit-agent-helper-1 via socket activation or setuid fallback so PAM runs as root, instead of the old direct Respond call that only works for uid 0.
This commit is contained in:
1 file changed
+287
-68
+287
-68
@@ -5,13 +5,20 @@ and listens for authentication requests from polkitd. Emits signals
|
||||
so the UI layer can show a password dialog.
|
||||
|
||||
D-Bus flow:
|
||||
1. Export org.freedesktop.PolicyKit1.AuthenticationAgent on session bus
|
||||
1. Export org.freedesktop.PolicyKit1.AuthenticationAgent on system bus
|
||||
2. Register with Polkit Authority on system bus
|
||||
3. polkitd calls BeginAuthentication → service emits signal
|
||||
4. UI responds via respond(cookie, password)
|
||||
|
||||
polkitd addresses the agent by the system-bus unique name it called
|
||||
RegisterAuthenticationAgent with, so the agent object must be exported
|
||||
on the system bus (not the session bus).
|
||||
"""
|
||||
|
||||
import os
|
||||
import pwd
|
||||
import socket
|
||||
import subprocess
|
||||
|
||||
from fabric.core.service import Service, Signal
|
||||
from fabric.utils import Gio, GLib, logger
|
||||
@@ -23,6 +30,10 @@ POLKIT_AUTHORITY_IFACE = "org.freedesktop.PolicyKit1.Authority"
|
||||
AGENT_OBJECT_PATH = "/org/freedesktop/PolicyKit1/AuthenticationAgent"
|
||||
AGENT_INTERFACE = "org.freedesktop.PolicyKit1.AuthenticationAgent"
|
||||
|
||||
AGENT_HELPER_SOCKET = "/run/polkit/agent-helper.socket"
|
||||
AGENT_HELPER_PATH = "/usr/lib/polkit-1/polkit-agent-helper-1"
|
||||
HELPER_TIMEOUT = 15
|
||||
|
||||
AGENT_INTROSPECTION = """<!DOCTYPE node>
|
||||
<node>
|
||||
<interface name="org.freedesktop.PolicyKit1.AuthenticationAgent">
|
||||
@@ -32,7 +43,7 @@ AGENT_INTROSPECTION = """<!DOCTYPE node>
|
||||
<arg name="icon_name" type="s" direction="in"/>
|
||||
<arg name="details" type="a{ss}" direction="in"/>
|
||||
<arg name="cookie" type="s" direction="in"/>
|
||||
<arg name="identities" type="a(si)" direction="in"/>
|
||||
<arg name="identities" type="a(sa{sv})" direction="in"/>
|
||||
</method>
|
||||
<method name="CancelAuthentication">
|
||||
<arg name="cookie" type="s" direction="in"/>
|
||||
@@ -85,33 +96,35 @@ class SysauthService(Service):
|
||||
self._initialized = True
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._session_bus_id = None
|
||||
self._agent_registration_id = None
|
||||
self._authority_proxy = None
|
||||
self._registered = False
|
||||
self._registered_subject = None
|
||||
self._pending = {}
|
||||
|
||||
def start(self) -> bool:
|
||||
"""Start the agent: export on session bus and register with Authority.
|
||||
"""Start the agent: export on system bus and register with Authority.
|
||||
|
||||
Returns:
|
||||
True if registration succeeded, False otherwise.
|
||||
"""
|
||||
try:
|
||||
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
|
||||
if bus is None:
|
||||
logger.error("[SysauthService] Cannot get session bus")
|
||||
logger.error("[SysauthService] Cannot get system bus")
|
||||
return False
|
||||
|
||||
node_info = Gio.DBusNodeInfo.new_for_xml(AGENT_INTROSPECTION)
|
||||
interface_info = node_info.lookup_interface(AGENT_INTERFACE)
|
||||
|
||||
self._session_bus_id = bus.register_object_with_closures2(
|
||||
self._agent_registration_id = bus.register_object_with_closures2(
|
||||
AGENT_OBJECT_PATH,
|
||||
interface_info,
|
||||
self._handle_method_call,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[SysauthService] Exported agent on session bus at {AGENT_OBJECT_PATH}"
|
||||
f"[SysauthService] Exported agent on system bus at {AGENT_OBJECT_PATH}"
|
||||
)
|
||||
|
||||
if not self._register_with_authority():
|
||||
@@ -129,50 +142,165 @@ class SysauthService(Service):
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Unregister and clean up D-Bus resources."""
|
||||
if self._session_bus_id is not None:
|
||||
if self._agent_registration_id is not None:
|
||||
try:
|
||||
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
|
||||
if bus is not None:
|
||||
bus.unregister_object(self._session_bus_id)
|
||||
bus.unregister_object(self._agent_registration_id)
|
||||
except GLib.Error:
|
||||
pass
|
||||
self._session_bus_id = None
|
||||
self._agent_registration_id = None
|
||||
|
||||
if self._registered:
|
||||
self._unregister_from_authority()
|
||||
self._registered = False
|
||||
|
||||
for pending in self._pending.values():
|
||||
invocation = pending["invocation"]
|
||||
GLib.idle_add(lambda: invocation.return_value(None))
|
||||
self._pending.clear()
|
||||
|
||||
logger.info("[SysauthService] Stopped")
|
||||
|
||||
def respond(self, cookie: str, password: str) -> None:
|
||||
def respond(self, cookie: str, password: str) -> bool:
|
||||
"""Send authentication response back to polkitd.
|
||||
|
||||
Delegates to the polkit-agent-helper-1 helper (preferring the
|
||||
systemd socket-activation path), which performs the PAM
|
||||
authentication and invokes AuthenticationAgentResponse2/3 on the
|
||||
Authority as root. Direct D-Bus response calls only accept uid 0.
|
||||
|
||||
On success the held BeginAuthentication reply is returned, which
|
||||
is what completes the pending session in polkitd. On failure the
|
||||
reply stays pending so the UI can re-prompt with the same cookie.
|
||||
|
||||
Args:
|
||||
cookie: The authentication session cookie.
|
||||
password: The password to send.
|
||||
"""
|
||||
try:
|
||||
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
if bus is None:
|
||||
logger.error("[SysauthService] Cannot get session bus for response")
|
||||
return
|
||||
|
||||
bus.call_sync(
|
||||
POLKIT_BUS_NAME,
|
||||
AGENT_OBJECT_PATH,
|
||||
AGENT_INTERFACE,
|
||||
"Respond",
|
||||
GLib.Variant("(ss)", (cookie, password)),
|
||||
None,
|
||||
Gio.DBusCallFlags.NONE,
|
||||
-1,
|
||||
None,
|
||||
Returns:
|
||||
True if authentication succeeded, False otherwise.
|
||||
"""
|
||||
pending = self._pending.get(cookie)
|
||||
if pending is None:
|
||||
logger.warning(
|
||||
f"[SysauthService] respond() called for unknown cookie {cookie[:16]}"
|
||||
)
|
||||
except GLib.Error as e:
|
||||
logger.error(f"[SysauthService] Failed to send response: {e}")
|
||||
return False
|
||||
|
||||
uid = pending["uid"]
|
||||
invocation = pending["invocation"]
|
||||
|
||||
try:
|
||||
username = pwd.getpwuid(uid).pw_name
|
||||
except (KeyError, OverflowError):
|
||||
logger.error(f"[SysauthService] No user found for uid {uid}")
|
||||
return False
|
||||
|
||||
success = self._run_helper(cookie, password, username)
|
||||
|
||||
if success:
|
||||
self._pending.pop(cookie, None)
|
||||
GLib.idle_add(lambda: invocation.return_value(None))
|
||||
|
||||
GLib.idle_add(lambda: self.emit("authentication-completed", cookie, success))
|
||||
return success
|
||||
|
||||
def cancel(self, cookie: str) -> None:
|
||||
"""Cancel a pending authentication request from the UI side.
|
||||
|
||||
Returns the held BeginAuthentication reply with a cancelled
|
||||
error so polkitd marks the session as dismissed.
|
||||
"""
|
||||
pending = self._pending.pop(cookie, None)
|
||||
if pending is None:
|
||||
return
|
||||
|
||||
invocation = pending["invocation"]
|
||||
GLib.idle_add(
|
||||
lambda: invocation.return_error_literal(
|
||||
"org.freedesktop.PolicyKit1.Error.Cancelled",
|
||||
"Authentication cancelled by user",
|
||||
)
|
||||
)
|
||||
GLib.idle_add(lambda: self.emit("authentication-cancelled", cookie))
|
||||
|
||||
def _run_helper(self, cookie: str, password: str, username: str) -> bool:
|
||||
"""Run polkit-agent-helper-1 via socket activation or setuid binary."""
|
||||
if os.path.exists(AGENT_HELPER_SOCKET):
|
||||
try:
|
||||
return self._run_helper_via_socket(cookie, password, username)
|
||||
except (TimeoutError, OSError) as e:
|
||||
logger.warning(
|
||||
f"[SysauthService] Socket helper failed, falling back to "
|
||||
f"setuid helper: {e}"
|
||||
)
|
||||
|
||||
if not os.path.exists(AGENT_HELPER_PATH):
|
||||
logger.error(
|
||||
f"[SysauthService] No authentication helper available at "
|
||||
f"{AGENT_HELPER_PATH}"
|
||||
)
|
||||
return False
|
||||
|
||||
return self._run_helper_setuid(cookie, password, username)
|
||||
|
||||
def _run_helper_via_socket(self, cookie: str, password: str, username: str) -> bool:
|
||||
"""Authenticate through the polkit-agent-helper systemd socket.
|
||||
|
||||
The helper reads the username and cookie from the first two input
|
||||
lines, then the password when PAM prompts for it. Answering each
|
||||
prompt keeps the conversation flowing.
|
||||
"""
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.settimeout(HELPER_TIMEOUT)
|
||||
with sock, sock.makefile("rwb") as stream:
|
||||
sock.connect(AGENT_HELPER_SOCKET)
|
||||
stream.write(username.encode() + b"\n")
|
||||
stream.write(cookie.encode() + b"\n")
|
||||
stream.write(password.encode() + b"\n")
|
||||
stream.flush()
|
||||
|
||||
answered = False
|
||||
for raw in stream:
|
||||
line = raw.decode(errors="replace").strip()
|
||||
if line.startswith("PAM_PROMPT_"):
|
||||
stream.write((password if not answered else "").encode() + b"\n")
|
||||
stream.flush()
|
||||
answered = True
|
||||
elif line == "SUCCESS":
|
||||
return True
|
||||
elif line == "FAILURE":
|
||||
return False
|
||||
return False
|
||||
|
||||
def _run_helper_setuid(self, cookie: str, password: str, username: str) -> bool:
|
||||
"""Authenticate through the setuid polkit-agent-helper-1 binary."""
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[AGENT_HELPER_PATH, username, cookie],
|
||||
input=password + "\n",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=HELPER_TIMEOUT,
|
||||
check=False,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("[SysauthService] Setuid helper timed out")
|
||||
return False
|
||||
|
||||
output = (proc.stdout or "").strip()
|
||||
return output.endswith("SUCCESS")
|
||||
|
||||
def _register_with_authority(self) -> bool:
|
||||
"""Register this agent with the Polkit Authority on the system bus."""
|
||||
"""Register this agent with the Polkit Authority on the system bus.
|
||||
|
||||
polkit's Authority exposes the eggdbus-style signature
|
||||
``RegisterAuthenticationAgent((sa{sv}) subject, s locale,
|
||||
s object_path)``. A session subject (matching how desktop agents
|
||||
register) is preferred so every request in the session routes to
|
||||
this agent; the process subject is the fallback.
|
||||
"""
|
||||
try:
|
||||
system_bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
|
||||
if system_bus is None:
|
||||
@@ -189,28 +317,60 @@ class SysauthService(Service):
|
||||
None,
|
||||
)
|
||||
|
||||
pid = os.getpid()
|
||||
locale = os.environ.get("LANG", "en_US.UTF-8")
|
||||
|
||||
builder = GLib.VariantBuilder(GLib.VariantType("(sa{sv}so)"))
|
||||
builder.add_value(GLib.Variant("s", "unix-process"))
|
||||
details = GLib.VariantBuilder(GLib.VariantType("a{sv}"))
|
||||
details.add_value(GLib.Variant("{sv}", ("pid", GLib.Variant("u", pid))))
|
||||
builder.add_value(details.end())
|
||||
builder.add_value(GLib.Variant("s", locale))
|
||||
builder.add_value(GLib.Variant("o", AGENT_OBJECT_PATH))
|
||||
candidates = [self._process_subject()]
|
||||
session_subject = self._session_subject()
|
||||
if session_subject is not None:
|
||||
candidates.insert(0, session_subject)
|
||||
|
||||
self._authority_proxy.call_sync(
|
||||
"RegisterAuthenticationAgent",
|
||||
builder.end(),
|
||||
Gio.DBusCallFlags.NONE,
|
||||
-1,
|
||||
None,
|
||||
)
|
||||
for kind, details in candidates:
|
||||
builder = GLib.VariantBuilder(GLib.VariantType("((sa{sv})ss)"))
|
||||
builder.add_value(self._subject_variant(kind, details))
|
||||
builder.add_value(GLib.Variant("s", locale))
|
||||
builder.add_value(GLib.Variant("s", AGENT_OBJECT_PATH))
|
||||
try:
|
||||
self._authority_proxy.call_sync(
|
||||
"RegisterAuthenticationAgent",
|
||||
builder.end(),
|
||||
Gio.DBusCallFlags.NONE,
|
||||
-1,
|
||||
None,
|
||||
)
|
||||
except GLib.Error as e:
|
||||
error_msg = str(e)
|
||||
if kind == "unix-session" and "already exists" in error_msg:
|
||||
logger.warning(
|
||||
"[SysauthService] Session agent already registered "
|
||||
"(another agent owns this session). Falling back to "
|
||||
"process-scope registration: only requests originating "
|
||||
"from this process will route here."
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[SysauthService] Register with {kind} subject failed: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
self._registered = True
|
||||
logger.info(f"[SysauthService] Registered with Authority (PID {pid})")
|
||||
return True
|
||||
self._registered = True
|
||||
self._registered_subject = (kind, details)
|
||||
|
||||
if kind == "unix-session":
|
||||
logger.info(
|
||||
"[SysauthService] Registered with Authority "
|
||||
f"(session subject, PID {os.getpid()})"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"[SysauthService] Registered with Authority using "
|
||||
f"process-scope subject (PID {os.getpid()}). "
|
||||
"Only requests from this process will be handled. "
|
||||
"To integrate with the full session, stop the "
|
||||
"existing agent (e.g. polkit-gnome-authentication-agent-1)."
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except GLib.Error as e:
|
||||
logger.error(f"[SysauthService] Registration failed: {e}")
|
||||
@@ -218,18 +378,15 @@ class SysauthService(Service):
|
||||
|
||||
def _unregister_from_authority(self) -> None:
|
||||
"""Unregister this agent from the Polkit Authority."""
|
||||
if self._authority_proxy is None:
|
||||
if self._authority_proxy is None or self._registered_subject is None:
|
||||
return
|
||||
|
||||
try:
|
||||
pid = os.getpid()
|
||||
kind, details = self._registered_subject
|
||||
|
||||
builder = GLib.VariantBuilder(GLib.VariantType("(sa{sv}o)"))
|
||||
builder.add_value(GLib.Variant("s", "unix-process"))
|
||||
details = GLib.VariantBuilder(GLib.VariantType("a{sv}"))
|
||||
details.add_value(GLib.Variant("{sv}", ("pid", GLib.Variant("u", pid))))
|
||||
builder.add_value(details.end())
|
||||
builder.add_value(GLib.Variant("o", AGENT_OBJECT_PATH))
|
||||
builder = GLib.VariantBuilder(GLib.VariantType("((sa{sv})s)"))
|
||||
builder.add_value(self._subject_variant(kind, details))
|
||||
builder.add_value(GLib.Variant("s", AGENT_OBJECT_PATH))
|
||||
|
||||
self._authority_proxy.call_sync(
|
||||
"UnregisterAuthenticationAgent",
|
||||
@@ -243,6 +400,63 @@ class SysauthService(Service):
|
||||
except GLib.Error as e:
|
||||
logger.warning(f"[SysauthService] Unregister failed: {e}")
|
||||
|
||||
def _session_subject(self) -> "tuple[str, dict] | None":
|
||||
"""Build a unix-session subject from the current session, if known."""
|
||||
session_id = os.environ.get("XDG_SESSION_ID")
|
||||
if not session_id:
|
||||
return None
|
||||
return ("unix-session", {"session-id": session_id})
|
||||
|
||||
def _process_subject(self) -> "tuple[str, dict]":
|
||||
"""Build a unix-process subject for this process.
|
||||
|
||||
polkit requires the ``start-time`` key (field 22 of
|
||||
``/proc/<pid>/stat``, in clock ticks) alongside ``pid`` to
|
||||
validate the subject.
|
||||
"""
|
||||
details: dict = {"pid": os.getpid()}
|
||||
start_time = self._process_start_time(os.getpid())
|
||||
if start_time is not None:
|
||||
details["start-time"] = start_time
|
||||
return ("unix-process", details)
|
||||
|
||||
@staticmethod
|
||||
def _process_start_time(pid: int) -> "int | None":
|
||||
"""Read the process start time from /proc (field 22, clock ticks)."""
|
||||
try:
|
||||
with open(f"/proc/{pid}/stat") as f:
|
||||
data = f.read()
|
||||
except OSError:
|
||||
return None
|
||||
name_end = data.rfind(")")
|
||||
if name_end < 0:
|
||||
return None
|
||||
fields = data[name_end + 2 :].split()
|
||||
if len(fields) < 20:
|
||||
return None
|
||||
try:
|
||||
return int(fields[19])
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
|
||||
def _subject_variant(self, kind: str, details: dict) -> GLib.Variant:
|
||||
"""Build the ``(sa{sv})`` subject struct for the Authority."""
|
||||
subject_builder = GLib.VariantBuilder(GLib.VariantType("(sa{sv})"))
|
||||
subject_builder.add_value(GLib.Variant("s", kind))
|
||||
|
||||
details_builder = GLib.VariantBuilder(GLib.VariantType("a{sv}"))
|
||||
for key, value in details.items():
|
||||
if key == "pid":
|
||||
variant = GLib.Variant("u", value)
|
||||
elif key == "start-time":
|
||||
variant = GLib.Variant("t", value)
|
||||
else:
|
||||
variant = GLib.Variant("s", str(value))
|
||||
details_builder.add_value(GLib.Variant("{sv}", (key, variant)))
|
||||
subject_builder.add_value(details_builder.end())
|
||||
|
||||
return subject_builder.end()
|
||||
|
||||
def _handle_method_call(
|
||||
self,
|
||||
connection: Gio.DBusConnection,
|
||||
@@ -270,7 +484,12 @@ class SysauthService(Service):
|
||||
parameters: GLib.Variant,
|
||||
invocation: Gio.DBusMethodInvocation,
|
||||
) -> None:
|
||||
"""Handle BeginAuthentication call from polkitd."""
|
||||
"""Handle BeginAuthentication call from polkitd.
|
||||
|
||||
The D-Bus reply is deliberately held back: polkitd completes the
|
||||
authentication session only when this method returns, so the
|
||||
reply is sent once authentication resolves (see respond()/cancel()).
|
||||
"""
|
||||
action_id, message, icon_name, _details, cookie, identities = (
|
||||
parameters.unpack()
|
||||
)
|
||||
@@ -278,18 +497,15 @@ class SysauthService(Service):
|
||||
uid = -1
|
||||
if identities:
|
||||
for kind, value in identities:
|
||||
if kind == "unix-user":
|
||||
if isinstance(value, dict):
|
||||
uid = value.get("uid", -1)
|
||||
elif isinstance(value, int):
|
||||
uid = value
|
||||
elif isinstance(value, tuple):
|
||||
uid = value[0] if value else -1
|
||||
if kind == "unix-user" and isinstance(value, dict):
|
||||
uid = value.get("uid", -1)
|
||||
break
|
||||
|
||||
if uid == -1:
|
||||
uid = os.getuid()
|
||||
|
||||
self._pending[cookie] = {"uid": uid, "invocation": invocation}
|
||||
|
||||
processed_action = _process_action_id(action_id, message)
|
||||
|
||||
logger.info(
|
||||
@@ -297,8 +513,6 @@ class SysauthService(Service):
|
||||
f"uid={uid} cookie={cookie[:16]}..."
|
||||
)
|
||||
|
||||
invocation.return_value(None)
|
||||
|
||||
GLib.idle_add(
|
||||
lambda: self.emit(
|
||||
"begin-authentication",
|
||||
@@ -318,6 +532,11 @@ class SysauthService(Service):
|
||||
"""Handle CancelAuthentication call from polkitd."""
|
||||
(cookie,) = parameters.unpack()
|
||||
logger.info(f"[SysauthService] CancelAuthentication: cookie={cookie[:16]}")
|
||||
|
||||
pending = self._pending.pop(cookie, None)
|
||||
if pending is not None:
|
||||
GLib.idle_add(lambda: pending["invocation"].return_value(None))
|
||||
|
||||
invocation.return_value(None)
|
||||
GLib.idle_add(lambda: self.emit("authentication-cancelled", cookie))
|
||||
|
||||
|
||||
Reference in new issue
Block a user