Skip to main content

crabshell_services/
polkit.rs

1//! A polkit authentication agent, as Quickshell's `PolkitAgent`: when a
2//! program asks polkit for something only some users may do, polkit asks
3//! the shell, and a config shows the "authenticate to do X" dialog.
4//!
5//! The shell registers with polkit on the system bus the first time a
6//! config asks for the agent. zbus has no object server here, so polkit's
7//! calls are answered by hand on the main thread. Each try runs polkit's
8//! setuid helper, which asks PAM: its prompts go into the request's state
9//! and the config's answers into its input. Nothing polls: the shell
10//! wakes when polkit calls or the helper prints.
11
12mod helper;
13#[cfg(test)]
14mod tests;
15
16use std::cell::{Cell, RefCell};
17use std::collections::{HashMap, VecDeque};
18use std::future::poll_fn;
19use std::pin::Pin;
20use std::rc::Rc;
21use std::sync::Once;
22
23use calloop::Interest;
24use crabshell_reactive::{ReadSignal, Signal};
25use crabshell_ui::Cx;
26use zbus::export::futures_core::Stream;
27use zbus::message::{Flags, Type as Kind};
28use zbus::{Connection, Message, MessageStream, OwnedVariant, Variant};
29
30use self::helper::{Helper, Line, Secret};
31use crate::Shared;
32use crate::dbus::{CALL_TIMEOUT, bus_call, owner_changed, owner_rule, set_changed};
33
34const AUTHORITY: &str = "org.freedesktop.PolicyKit1";
35const AUTHORITY_PATH: &str = "/org/freedesktop/PolicyKit1/Authority";
36const AUTHORITY_INTERFACE: &str = "org.freedesktop.PolicyKit1.Authority";
37const AGENT_INTERFACE: &str = "org.freedesktop.PolicyKit1.AuthenticationAgent";
38/// Where the agent answers polkit: any path of the shell's own.
39const AGENT_PATH: &str = "/org/crabshell/PolkitAgent";
40
41/// Where polkit's helper is on most distributions.
42/// shortcut: one path, so a distribution that keeps it in
43/// `/usr/libexec` gets no dialogs; look in both if one turns up.
44pub(crate) const HELPER: &str = "/usr/lib/polkit-1/polkit-agent-helper-1";
45
46const FAILED: &str = "org.freedesktop.PolicyKit1.Error.Failed";
47const CANCELLED: &str = "org.freedesktop.PolicyKit1.Error.Cancelled";
48const ACCESS_DENIED: &str = "org.freedesktop.DBus.Error.AccessDenied";
49const INVALID_ARGS: &str = "org.freedesktop.DBus.Error.InvalidArgs";
50const UNKNOWN_METHOD: &str = "org.freedesktop.DBus.Error.UnknownMethod";
51
52/// The most bytes kept of a string from polkit or its helper.
53const MAX_TEXT: usize = 4 * 1024;
54/// The most wrong answers to one request before polkit is told it failed.
55const MAX_TRIES: u32 = 3;
56/// The most identities kept of a request.
57const MAX_IDENTITIES: usize = 64;
58/// The most requests waiting behind the one answered.
59const MAX_QUEUED: usize = 8;
60/// The most info and error lines a request keeps, the newest.
61const MAX_LINES: usize = 16;
62
63/// A request from polkit: a program asked to do `action_id`, and polkit
64/// wants a user to prove they may.
65#[derive(Clone, Debug, PartialEq)]
66pub struct AuthRequest {
67  /// Which request this is, so two alike still differ. polkit's cookie
68  /// stays in the agent.
69  id: u64,
70  /// What was asked for, such as `org.freedesktop.login1.reboot`.
71  pub action_id: String,
72  /// What to tell the user, in the shell's language, at most 4 KiB.
73  pub message: String,
74  /// A themed icon for the action, or empty.
75  pub icon_name: String,
76  /// The users who may answer, by name. The first is the one asked now;
77  /// [`PolkitAgent::pick_identity`] puts another first.
78  pub identities: Vec<String>,
79  /// Where the conversation is.
80  pub state: AuthState,
81  /// What PAM told the user this try, such as to touch a key, oldest
82  /// first. At most 16 lines of 4 KiB.
83  pub info: Vec<String>,
84  /// What PAM said went wrong this try, oldest first, kept as `info` is.
85  pub errors: Vec<String>,
86}
87
88/// Where a request's conversation is.
89#[derive(Clone, Debug, PartialEq)]
90pub enum AuthState {
91  /// polkit's helper is starting, or has only told the user something.
92  Waiting,
93  /// PAM asks `message`, such as `Password: `, for
94  /// [`PolkitAgent::respond`]. `visible` is whether the answer may show
95  /// as it is typed.
96  Asking {
97    /// The question, at most 4 KiB.
98    message: String,
99    /// `false` for a password.
100    visible: bool,
101  },
102  /// The answer is being checked.
103  Busy,
104  /// The answer was wrong. [`PolkitAgent::respond`] tries again; after
105  /// the third failure polkit is told, and the request goes.
106  Failed {
107    /// What PAM said, or a plain "Authentication failed".
108    message: String,
109  },
110}
111
112/// The shell's polkit agent, from `cx.polkit_agent()`. A clone is cheap
113/// and shares the one agent.
114#[derive(Clone)]
115pub struct PolkitAgent {
116  /// `None` when the shell provided no services.
117  service: Option<Rc<Service>>,
118  request: ReadSignal<Option<AuthRequest>>,
119}
120
121impl PolkitAgent {
122  /// The request being answered now, or `None`. One shows at a time; up
123  /// to 8 more wait their turn, and polkit is told any past that failed.
124  pub fn request(&self) -> ReadSignal<Option<AuthRequest>> {
125    self.request
126  }
127
128  /// Answers the request's question, as the user typed it. After a
129  /// failure, it tries again and answers the new try's first question.
130  /// At any other time, or with a line break in it, it is dropped. It is
131  /// never logged, and its bytes are zeroed once sent.
132  pub fn respond(&self, text: impl Into<String>) {
133    let secret = Secret(text.into().into_bytes());
134    if let Some(service) = &self.service {
135      service.respond(secret);
136    }
137  }
138
139  /// Gives up on the request, as the user's cancel button does, and tells
140  /// polkit so.
141  pub fn cancel(&self) {
142    if let Some(service) = &self.service {
143      service.finish(Some((CANCELLED, "the user cancelled")));
144    }
145  }
146
147  /// Asks `name`, one of the request's identities, instead: it moves to
148  /// the front, and the conversation starts again as that user. An
149  /// unknown name is logged.
150  pub fn pick_identity(&self, name: &str) {
151    if let Some(service) = &self.service {
152      service.pick(name);
153    }
154  }
155}
156
157/// `cx.polkit_agent()`.
158pub(crate) fn polkit_agent(cx: &Cx) -> PolkitAgent {
159  let Some(service) = cx.service::<Service>() else {
160    static LOGGED: Once = Once::new();
161    LOGGED.call_once(|| {
162      tracing::warn!("cx.polkit_agent() needs the services the shell provides; no requests arrive");
163    });
164    return PolkitAgent {
165      service: None,
166      request: cx.signal(None).read_only(),
167    };
168  };
169  let request = service.start();
170  PolkitAgent {
171    service: Some(service),
172    request,
173  }
174}
175
176/// The polkit agent of one shell, started on first use.
177pub(crate) struct Service {
178  shared: Rc<Shared>,
179  state: RefCell<Option<State>>,
180  /// The number the next helper run and request get.
181  next: Cell<u64>,
182}
183
184struct State {
185  request: Signal<Option<AuthRequest>>,
186  /// The system bus, once connected.
187  connection: Option<Connection>,
188  /// polkit's unique name, once it took the registration: the only
189  /// caller the agent answers.
190  authority: Option<String>,
191  current: Option<Current>,
192  /// Requests that came while one was answered, oldest first.
193  queue: VecDeque<Begin>,
194}
195
196/// Stops the helper and forgets the requests when the root goes. The
197/// agent's connection closes at the next message on it, or with the shell.
198struct StopOnDrop(Rc<Service>);
199
200impl Drop for StopOnDrop {
201  fn drop(&mut self) {
202    self.0.forget();
203  }
204}
205
206/// A `BeginAuthentication` call, kept until it is answered.
207struct Begin {
208  call: Message,
209  cookie: String,
210  shown: AuthRequest,
211}
212
213/// The request being answered.
214struct Current {
215  begin: Begin,
216  failures: u32,
217  helper: Option<Helper>,
218  /// An answer given after a failure, for the next try's first question.
219  answer: Option<Secret>,
220}
221
222impl Current {
223  fn new(begin: Begin) -> Current {
224    Current {
225      begin,
226      failures: 0,
227      helper: None,
228      answer: None,
229    }
230  }
231}
232
233impl Service {
234  pub(crate) fn new(shared: Rc<Shared>) -> Service {
235    Service {
236      shared,
237      state: RefCell::new(None),
238      next: Cell::new(1),
239    }
240  }
241
242  fn next(&self) -> u64 {
243    let next = self.next.get();
244    self.next.set(next + 1);
245    next
246  }
247
248  /// The request, registering with polkit if this is the first ask.
249  fn start(self: &Rc<Self>) -> ReadSignal<Option<AuthRequest>> {
250    if let Some(state) = &*self.state.borrow() {
251      return state.request.read_only();
252    }
253    let request = self.shared.root.signal(None);
254    *self.state.borrow_mut() = Some(State {
255      request,
256      connection: None,
257      authority: None,
258      current: None,
259      queue: VecDeque::new(),
260    });
261    self.shared.root.signal(StopOnDrop(Rc::clone(self)));
262    match self.shared.config.system_bus.clone() {
263      Some(address) => self.shared.spawn(run(Rc::clone(self), address)),
264      None => tracing::info!("no system bus, so no polkit requests arrive"),
265    }
266    request.read_only()
267  }
268
269  /// Drops the connection and every request unanswered, as when the root
270  /// or the bus goes. Touches no signal, since the root may be going.
271  fn forget(&self) {
272    if let Some(state) = self.state.borrow_mut().as_mut() {
273      state.connection = None;
274    }
275    self.drop_requests();
276  }
277
278  /// Drops every request unanswered, stopping the helper, as when polkit
279  /// goes and nobody could answer. Touches no signal.
280  fn drop_requests(&self) {
281    let current = self.state.borrow_mut().as_mut().and_then(|state| {
282      state.queue.clear();
283      state.authority = None;
284      state.current.take()
285    });
286    if let Some(helper) = current.and_then(|current| current.helper) {
287      helper.stop(&self.shared, true);
288    }
289  }
290
291  /// Whether the request is still there: its owner, the root, isn't
292  /// disposed.
293  fn alive(&self) -> bool {
294    let state = self.state.borrow();
295    let alive = state
296      .as_ref()
297      .and_then(|state| state.request.try_with(|_| ()));
298    alive.is_some()
299  }
300
301  /// Runs `f` on the request being answered, if there is one.
302  fn with_current<R>(&self, f: impl FnOnce(&mut Current) -> R) -> Option<R> {
303    let mut state = self.state.borrow_mut();
304    state.as_mut()?.current.as_mut().map(f)
305  }
306
307  /// Shows the request as it is now.
308  fn publish(&self) {
309    let shown = self.state.borrow().as_ref().map(|state| {
310      let current = state.current.as_ref();
311      (
312        state.request,
313        current.map(|current| current.begin.shown.clone()),
314      )
315    });
316    if let Some((signal, shown)) = shown {
317      set_changed(signal, shown);
318    }
319  }
320
321  /// One method call from the bus.
322  fn on_call(self: &Rc<Self>, call: Message) {
323    let header = call.header();
324    let sender = header.sender().map(|sender| sender.as_str());
325    let authority = self
326      .state
327      .borrow()
328      .as_ref()
329      .and_then(|state| state.authority.clone());
330    if sender.is_none() || sender != authority.as_deref() {
331      tracing::warn!(
332        ?sender,
333        "a program other than polkit called the agent; refused"
334      );
335      return self.reply(
336        &call,
337        Some((ACCESS_DENIED, "only polkit may call the agent")),
338      );
339    }
340    let path = header.path().map(|path| path.as_str());
341    let interface = header.interface().map(|interface| interface.as_str());
342    let member = header.member().map(|member| member.as_str());
343    match (path, interface, member) {
344      (Some(AGENT_PATH), Some(AGENT_INTERFACE), Some("BeginAuthentication")) => self.begin(call),
345      (Some(AGENT_PATH), Some(AGENT_INTERFACE), Some("CancelAuthentication")) => {
346        self.cancelled(call);
347      }
348      _ => self.reply(&call, Some((UNKNOWN_METHOD, "no such method"))),
349    }
350  }
351
352  /// polkit asks the user to authenticate.
353  fn begin(self: &Rc<Self>, call: Message) {
354    let begin = match begin_args(&call, self.next()) {
355      Ok(begin) => begin,
356      Err((error, why)) => {
357        tracing::warn!(why, "a polkit request the agent can't answer");
358        return self.reply(&call, Some((error, why)));
359      }
360    };
361    let refused = {
362      let mut state = self.state.borrow_mut();
363      let Some(state) = state.as_mut() else {
364        return;
365      };
366      let action = &begin.shown.action_id;
367      tracing::info!(action, "polkit asks to authenticate");
368      if state.current.is_none() {
369        state.current = Some(Current::new(begin));
370        None
371      } else if state.queue.len() < MAX_QUEUED {
372        state.queue.push_back(begin);
373        return;
374      } else {
375        Some(begin)
376      }
377    };
378    match refused {
379      Some(begin) => {
380        tracing::warn!("{MAX_QUEUED} polkit requests wait already; refused another");
381        self.reply(&begin.call, Some((FAILED, "too many requests wait")));
382      }
383      None => self.run_helper(),
384    }
385  }
386
387  /// polkit takes back the request `cookie` names.
388  fn cancelled(self: &Rc<Self>, call: Message) {
389    let Ok(cookie) = call.body().deserialize::<String>() else {
390      return self.reply(&call, Some((INVALID_ARGS, "not a cookie")));
391    };
392    self.reply(&call, None);
393    let why = "polkit cancelled it";
394    let current = self.with_current(|current| current.begin.cookie == cookie);
395    if current == Some(true) {
396      tracing::info!("polkit cancelled the request");
397      return self.finish(Some((CANCELLED, why)));
398    }
399    let queued = self.state.borrow_mut().as_mut().and_then(|state| {
400      let at = state
401        .queue
402        .iter()
403        .position(|begin| begin.cookie == cookie)?;
404      state.queue.remove(at)
405    });
406    if let Some(begin) = queued {
407      self.reply(&begin.call, Some((CANCELLED, why)));
408    }
409  }
410
411  /// The config picks identity `name`.
412  fn pick(self: &Rc<Self>, name: &str) {
413    let picked = self.with_current(|current| {
414      let identities = &mut current.begin.shown.identities;
415      let at = identities.iter().position(|known| known == name)?;
416      identities[..=at].rotate_right(1);
417      current.answer = None;
418      Some(current.helper.take())
419    });
420    let Some(helper) = picked.flatten() else {
421      return tracing::warn!(name, "no such identity to pick for polkit");
422    };
423    if let Some(helper) = helper {
424      helper.stop(&self.shared, true);
425    }
426    self.run_helper();
427  }
428
429  /// Starts a try: polkit's helper, for the first identity.
430  fn run_helper(self: &Rc<Self>) {
431    let Some(event_loop) = self.shared.event_loop() else {
432      return;
433    };
434    let run = self.next();
435    let started = self.with_current(|current| {
436      let shown = &mut current.begin.shown;
437      shown.state = AuthState::Waiting;
438      shown.info.clear();
439      shown.errors.clear();
440      let program = &self.shared.config.polkit_helper;
441      let mut helper = Helper::start(program, &shown.identities[0], run)?;
442      if let Err(error) = helper.send(current.begin.cookie.as_bytes()) {
443        helper.stop(&self.shared, true);
444        return Err(error);
445      }
446      let service = Rc::clone(self);
447      let ready = Box::new(move || service.on_output(run));
448      helper.token = helper
449        .stdout_fd()
450        .and_then(|fd| event_loop.insert_fd(fd, Interest::READ, ready));
451      if helper.token.is_none() {
452        helper.stop(&self.shared, true);
453        return Err(std::io::Error::other("its output couldn't be watched"));
454      }
455      current.helper = Some(helper);
456      Ok(())
457    });
458    match started {
459      Some(Ok(())) => self.publish(),
460      Some(Err(error)) => {
461        let program = self.shared.config.polkit_helper.display();
462        tracing::warn!(%error, %program, "couldn't start polkit's helper");
463        self.finish(Some((FAILED, "polkit's helper couldn't start")));
464      }
465      None => {}
466    }
467  }
468
469  /// What the helper of run `run` printed.
470  fn on_output(self: &Rc<Self>, run: u64) {
471    let running = |service: &Service| {
472      let running =
473        service.with_current(|current| current.helper.as_ref().map(|helper| helper.run));
474      running.flatten() == Some(run)
475    };
476    if !running(self) {
477      return;
478    }
479    let read = self.with_current(|current| current.helper.as_mut()?.read());
480    let Some((lines, ended)) = read.flatten() else {
481      return;
482    };
483    for line in lines {
484      if !running(self) {
485        return;
486      }
487      self.on_line(helper::parse(&line));
488    }
489    if ended && running(self) {
490      tracing::warn!("polkit's helper ended without an answer");
491      self.failed();
492    }
493  }
494
495  fn on_line(self: &Rc<Self>, line: Line) {
496    let keep = |lines: &mut Vec<String>, text| {
497      if lines.len() == MAX_LINES {
498        lines.remove(0);
499      }
500      lines.push(text);
501    };
502    let verdict = line_verdict(&line);
503    let answer = self.with_current(|current| {
504      let shown = &mut current.begin.shown;
505      match line {
506        Line::Prompt { message, visible } => {
507          shown.state = AuthState::Asking { message, visible };
508          return current.answer.take();
509        }
510        Line::Info(text) => keep(&mut shown.info, text),
511        Line::Error(text) => keep(&mut shown.errors, text),
512        Line::Unknown => tracing::debug!("polkit's helper printed a line it doesn't know; skipped"),
513        Line::Success | Line::Failure => {}
514      }
515      None
516    });
517    match verdict {
518      Some(true) => {
519        tracing::info!("authenticated for polkit");
520        self.finish(None);
521      }
522      Some(false) => self.failed(),
523      None => match answer.flatten() {
524        // A try after a failure answers its first question at once.
525        Some(secret) => self.send(secret),
526        None => self.publish(),
527      },
528    }
529  }
530
531  /// A wrong answer, or a helper that ended without saying: shows it, or
532  /// after the third tells polkit.
533  fn failed(self: &Rc<Self>) {
534    let gave_up = self.with_current(|current| {
535      if let Some(helper) = current.helper.take() {
536        helper.stop(&self.shared, true);
537      }
538      current.answer = None;
539      current.failures += 1;
540      let shown = &mut current.begin.shown;
541      let message = shown.errors.last().cloned();
542      let message = message.unwrap_or_else(|| String::from("Authentication failed"));
543      shown.state = AuthState::Failed { message };
544      current.failures >= MAX_TRIES
545    });
546    match gave_up {
547      Some(true) => {
548        tracing::info!("polkit authentication failed {MAX_TRIES} times");
549        self.finish(Some((FAILED, "authentication failed")));
550      }
551      Some(false) => self.publish(),
552      None => {}
553    }
554  }
555
556  /// The config's answer.
557  fn respond(self: &Rc<Self>, secret: Secret) {
558    // A line break would end the line early, and the helper, in C, would
559    // read a NUL as the end.
560    if secret.0.contains(&b'\n') || secret.0.contains(&0) {
561      return tracing::warn!("a polkit answer with a line break or NUL was dropped");
562    }
563    let state = self.with_current(|current| current.begin.shown.state.clone());
564    match state {
565      Some(AuthState::Asking { .. }) => self.send(secret),
566      Some(AuthState::Failed { .. }) => {
567        self.with_current(|current| current.answer = Some(secret));
568        self.run_helper();
569      }
570      _ => tracing::debug!("no polkit question to answer; an answer was dropped"),
571    }
572  }
573
574  /// Sends `secret` to the helper, which then checks it. It goes when
575  /// this returns.
576  fn send(self: &Rc<Self>, secret: Secret) {
577    let sent = self.with_current(|current| {
578      let sent = current.helper.as_mut().map(|helper| helper.send(&secret.0));
579      if let Some(Ok(())) = sent {
580        current.begin.shown.state = AuthState::Busy;
581      }
582      sent
583    });
584    match sent.flatten() {
585      Some(Ok(())) => self.publish(),
586      Some(Err(error)) => {
587        tracing::warn!(%error, "couldn't answer polkit's helper");
588        self.failed();
589      }
590      None => {}
591    }
592  }
593
594  /// Ends the request being answered and tells polkit: `None` for
595  /// authenticated, else the error's name and why. The next waiting
596  /// request starts.
597  fn finish(self: &Rc<Self>, error: Option<(&'static str, &str)>) {
598    let current = self.state.borrow_mut().as_mut().and_then(|state| {
599      let current = state.current.take()?;
600      state.current = state.queue.pop_front().map(Current::new);
601      Some(current)
602    });
603    let Some(current) = current else {
604      return;
605    };
606    if let Some(helper) = current.helper {
607      // After a success it is telling polkit, so it is left to end.
608      helper.stop(&self.shared, error.is_some());
609    }
610    self.reply(&current.begin.call, error);
611    match self.with_current(|_| ()) {
612      Some(()) => self.run_helper(),
613      None => self.publish(),
614    }
615  }
616
617  /// Answers `call`: with an empty return for `None`, else with the error
618  /// and why.
619  fn reply(&self, call: &Message, error: Option<(&str, &str)>) {
620    let header = call.header();
621    if header.primary().flags().contains(Flags::NO_REPLY_EXPECTED) {
622      return;
623    }
624    let reply = match error {
625      None => Message::method_return(&header).build(&()),
626      Some((name, why)) => Message::error(&header, name).build(&why),
627    };
628    let connection = self
629      .state
630      .borrow()
631      .as_ref()
632      .and_then(|state| state.connection.clone());
633    let Some(connection) = connection else {
634      return;
635    };
636    self.shared.spawn(async move {
637      let sent = match reply {
638        Ok(reply) => connection.send(&reply).await,
639        Err(error) => Err(error),
640      };
641      if let Err(error) = sent {
642        tracing::debug!(%error, "the polkit agent's answer wasn't sent");
643      }
644    });
645  }
646}
647
648/// Whether `line` ends the try: `true` for a success, `false` for a
649/// failure.
650fn line_verdict(line: &Line) -> Option<bool> {
651  match line {
652    Line::Success => Some(true),
653    Line::Failure => Some(false),
654    _ => None,
655  }
656}
657
658/// `BeginAuthentication`'s arguments: the action, the message, the icon,
659/// details, the cookie and the identities.
660type BeginArgs<'a> = (
661  &'a str,
662  &'a str,
663  &'a str,
664  HashMap<&'a str, &'a str>,
665  &'a str,
666  Vec<(&'a str, HashMap<&'a str, OwnedVariant>)>,
667);
668
669/// The request `call` asks for, numbered `id`, or the error to answer.
670fn begin_args(call: &Message, id: u64) -> Result<Begin, (&'static str, &'static str)> {
671  let body = call.body();
672  let Ok((action_id, message, icon_name, _, cookie, identities)) =
673    body.deserialize::<BeginArgs<'_>>()
674  else {
675    return Err((INVALID_ARGS, "not a polkit request"));
676  };
677  // It is the helper's first line of input, so it can't hold another.
678  if cookie.is_empty() || cookie.len() > MAX_TEXT || cookie.contains('\n') {
679    return Err((FAILED, "a bad cookie"));
680  }
681  let passwd = std::fs::read_to_string("/etc/passwd").unwrap_or_default();
682  let mut users: Vec<(u32, String)> = identities
683    .iter()
684    .take(MAX_IDENTITIES)
685    .filter(|(kind, _)| *kind == "unix-user")
686    .filter_map(|(_, details)| u32::try_from(details.get("uid")?).ok())
687    .filter_map(|uid| Some((uid, user_name(&passwd, uid)?)))
688    .collect();
689  // The session's user is asked first: polkit may list root first, as for
690  // an admin group.
691  let own = rustix::process::getuid().as_raw();
692  if let Some(at) = users.iter().position(|(uid, _)| *uid == own) {
693    users[..=at].rotate_right(1);
694  }
695  let names: Vec<String> = users.into_iter().map(|(_, name)| name).collect();
696  if names.is_empty() {
697    return Err((FAILED, "no user the agent knows may answer"));
698  }
699  Ok(Begin {
700    call: call.clone(),
701    cookie: cookie.to_owned(),
702    shown: AuthRequest {
703      id,
704      action_id: cut(action_id, MAX_TEXT),
705      message: cut(message, MAX_TEXT),
706      icon_name: cut(icon_name, MAX_TEXT),
707      identities: names,
708      state: AuthState::Waiting,
709      info: Vec::new(),
710      errors: Vec::new(),
711    },
712  })
713}
714
715/// `text`'s first `max` bytes at most, cut at a char boundary.
716fn cut(text: &str, max: usize) -> String {
717  text[..text.floor_char_boundary(max)].to_owned()
718}
719
720/// The name of user `uid`, from `passwd`, the text of `/etc/passwd`, or
721/// for this shell's own user from `USER`.
722/// shortcut: reads only `/etc/passwd`, so another user from LDAP or
723/// systemd-homed isn't offered; ask NSS through a crate if that matters.
724fn user_name(passwd: &str, uid: u32) -> Option<String> {
725  let listed = passwd.lines().find_map(|line| {
726    let mut fields = line.split(':');
727    let name = fields.next()?;
728    let uid_field = fields.nth(1)?;
729    (uid_field.parse() == Ok(uid)).then(|| name.to_owned())
730  });
731  let own = || {
732    let ours = rustix::process::getuid().as_raw() == uid;
733    ours.then(|| std::env::var("USER").ok()).flatten()
734  };
735  listed.or_else(own).filter(|name| !name.is_empty())
736}
737
738/// Who the agent answers for: this login session when `session` names
739/// one, as `XDG_SESSION_ID` does, else this process.
740fn subject(session: Option<String>) -> (&'static str, HashMap<&'static str, Variant<'static>>) {
741  match session {
742    Some(id) => (
743      "unix-session",
744      HashMap::from([("session-id", Variant::from(id))]),
745    ),
746    None => (
747      "unix-process",
748      HashMap::from([
749        ("pid", Variant::from(std::process::id())),
750        ("start-time", Variant::from(start_time().unwrap_or(0))),
751      ]),
752    ),
753  }
754}
755
756/// When this process started, in clock ticks after boot, as polkit names
757/// a process: the 22nd field of `/proc/self/stat`.
758fn start_time() -> Option<u64> {
759  let stat = std::fs::read_to_string("/proc/self/stat").ok()?;
760  // Fields after the name in parentheses start at the 3rd.
761  stat
762    .rsplit_once(')')?
763    .1
764    .split_whitespace()
765    .nth(19)?
766    .parse()
767    .ok()
768}
769
770/// Joins the system bus at `address`, registers the agent with polkit,
771/// again whenever polkit starts again, and answers polkit's calls, until
772/// the bus closes or the request is disposed.
773async fn run(service: Rc<Service>, address: zbus::Address) {
774  let connection = match zbus::connection::Builder::address(address).build().await {
775    Ok(connection) => connection,
776    Err(error) => {
777      return tracing::info!(%error, "can't reach the system bus, so no polkit requests arrive");
778    }
779  };
780  // Made before polkit can call, so no call is missed.
781  let mut messages = MessageStream::from(&connection);
782  if let Err(error) = bus_call(&connection, "AddMatch", &owner_rule(AUTHORITY)).await {
783    return tracing::warn!(%error, "can't follow polkit on the system bus, so no requests arrive");
784  }
785  if let Some(state) = service.state.borrow_mut().as_mut() {
786    state.connection = Some(connection.clone());
787  }
788  register(&service, &connection).await;
789  loop {
790    let next = poll_fn(|cx| Pin::new(&mut messages).poll_next(cx)).await;
791    if !service.alive() {
792      return;
793    }
794    match next {
795      Some(Ok(message)) if message.header().message_type() == Kind::MethodCall => {
796        service.on_call(message);
797      }
798      Some(Ok(message)) => {
799        // The bus daemon sends this, so nobody else can fake it.
800        if let Some((name, owner)) = owner_changed(&message)
801          && name == AUTHORITY
802        {
803          // Its requests went with it.
804          service.drop_requests();
805          service.publish();
806          if !owner.is_empty() {
807            tracing::info!("polkit started again; registering the agent with it");
808            register(&service, &connection).await;
809          }
810        }
811      }
812      Some(Err(error)) => tracing::warn!(%error, "a bad message on the system bus"),
813      None => break,
814    }
815  }
816  tracing::warn!("the system bus closed, so no more polkit requests arrive");
817  service.forget();
818  service.publish();
819}
820
821/// Registers the agent with the polkit on `connection`, which takes its
822/// calls only from the unique name that answered. A refusal is logged.
823async fn register(service: &Rc<Service>, connection: &Connection) {
824  let session = std::env::var("XDG_SESSION_ID")
825    .ok()
826    .filter(|id| !id.is_empty());
827  // The locale desktop entries are read in, so polkit's message matches.
828  let locale = service.shared.config.desktop_entries.locale.as_str();
829  let body = (subject(session), locale, AGENT_PATH);
830  let call = connection.call_method(
831    Some(AUTHORITY),
832    AUTHORITY_PATH,
833    Some(AUTHORITY_INTERFACE),
834    "RegisterAuthenticationAgent",
835    &body,
836  );
837  let authority = match service.shared.timeout(CALL_TIMEOUT, call).await {
838    Ok(reply) => reply.header().sender().map(|sender| sender.to_string()),
839    Err(error) => return tracing::warn!(%error, "polkit refused the agent, so no requests arrive"),
840  };
841  tracing::info!("registered as the polkit agent");
842  if let Some(state) = service.state.borrow_mut().as_mut() {
843    state.authority = authority;
844  }
845}