1mod 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";
38const AGENT_PATH: &str = "/org/crabshell/PolkitAgent";
40
41pub(crate) const HELPER: &str = "/usr/lib/polkit-1/polkit-agent-helper-1";
45
46pub(crate) const HELPER_SOCKET: &str = "/run/polkit/agent-helper.socket";
49
50const FAILED: &str = "org.freedesktop.PolicyKit1.Error.Failed";
51const CANCELLED: &str = "org.freedesktop.PolicyKit1.Error.Cancelled";
52const ACCESS_DENIED: &str = "org.freedesktop.DBus.Error.AccessDenied";
53const INVALID_ARGS: &str = "org.freedesktop.DBus.Error.InvalidArgs";
54const UNKNOWN_METHOD: &str = "org.freedesktop.DBus.Error.UnknownMethod";
55
56const MAX_TEXT: usize = 4 * 1024;
58const MAX_TRIES: u32 = 3;
60const MAX_IDENTITIES: usize = 64;
62const MAX_QUEUED: usize = 8;
64const MAX_LINES: usize = 16;
66
67#[derive(Clone, Debug, PartialEq)]
70pub struct AuthRequest {
71 id: u64,
74 pub action_id: String,
76 pub message: String,
78 pub icon_name: String,
80 pub identities: Vec<String>,
83 pub state: AuthState,
85 pub info: Vec<String>,
88 pub errors: Vec<String>,
90}
91
92#[derive(Clone, Debug, PartialEq)]
94pub enum AuthState {
95 Waiting,
97 Asking {
101 message: String,
103 visible: bool,
105 },
106 Busy,
108 Failed {
111 message: String,
113 },
114}
115
116#[derive(Clone)]
119pub struct PolkitAgent {
120 service: Option<Rc<Service>>,
122 request: ReadSignal<Option<AuthRequest>>,
123}
124
125impl PolkitAgent {
126 pub fn request(&self) -> ReadSignal<Option<AuthRequest>> {
129 self.request
130 }
131
132 pub fn respond(&self, text: impl Into<String>) {
137 let secret = Secret(text.into().into_bytes());
138 if let Some(service) = &self.service {
139 service.respond(secret);
140 }
141 }
142
143 pub fn cancel(&self) {
146 if let Some(service) = &self.service {
147 service.finish(Some((CANCELLED, "the user cancelled")));
148 }
149 }
150
151 pub fn pick_identity(&self, name: &str) {
155 if let Some(service) = &self.service {
156 service.pick(name);
157 }
158 }
159}
160
161pub(crate) fn polkit_agent(cx: &Cx) -> PolkitAgent {
163 let Some(service) = cx.service::<Service>() else {
164 static LOGGED: Once = Once::new();
165 LOGGED.call_once(|| {
166 tracing::warn!("cx.polkit_agent() needs the services the shell provides; no requests arrive");
167 });
168 return PolkitAgent {
169 service: None,
170 request: cx.signal(None).read_only(),
171 };
172 };
173 let request = service.start();
174 PolkitAgent {
175 service: Some(service),
176 request,
177 }
178}
179
180pub(crate) struct Service {
182 shared: Rc<Shared>,
183 state: RefCell<Option<State>>,
184 next: Cell<u64>,
186}
187
188struct State {
189 request: Signal<Option<AuthRequest>>,
190 connection: Option<Connection>,
192 authority: Option<String>,
195 current: Option<Current>,
196 queue: VecDeque<Begin>,
198}
199
200struct StopOnDrop(Rc<Service>);
203
204impl Drop for StopOnDrop {
205 fn drop(&mut self) {
206 self.0.forget();
207 }
208}
209
210struct Begin {
212 call: Message,
213 cookie: String,
214 shown: AuthRequest,
215}
216
217struct Current {
219 begin: Begin,
220 failures: u32,
221 helper: Option<Helper>,
222 answer: Option<Secret>,
224}
225
226impl Current {
227 fn new(begin: Begin) -> Current {
228 Current {
229 begin,
230 failures: 0,
231 helper: None,
232 answer: None,
233 }
234 }
235}
236
237impl Service {
238 pub(crate) fn new(shared: Rc<Shared>) -> Service {
239 Service {
240 shared,
241 state: RefCell::new(None),
242 next: Cell::new(1),
243 }
244 }
245
246 fn next(&self) -> u64 {
247 let next = self.next.get();
248 self.next.set(next + 1);
249 next
250 }
251
252 fn start(self: &Rc<Self>) -> ReadSignal<Option<AuthRequest>> {
254 if let Some(state) = &*self.state.borrow() {
255 return state.request.read_only();
256 }
257 let request = self.shared.root.signal(None);
258 *self.state.borrow_mut() = Some(State {
259 request,
260 connection: None,
261 authority: None,
262 current: None,
263 queue: VecDeque::new(),
264 });
265 self.shared.root.signal(StopOnDrop(Rc::clone(self)));
266 match self.shared.config.system_bus.clone() {
267 Some(address) => self.shared.spawn(run(Rc::clone(self), address)),
268 None => tracing::info!("no system bus, so no polkit requests arrive"),
269 }
270 request.read_only()
271 }
272
273 fn forget(&self) {
276 if let Some(state) = self.state.borrow_mut().as_mut() {
277 state.connection = None;
278 }
279 self.drop_requests();
280 }
281
282 fn drop_requests(&self) {
285 let current = self.state.borrow_mut().as_mut().and_then(|state| {
286 state.queue.clear();
287 state.authority = None;
288 state.current.take()
289 });
290 if let Some(helper) = current.and_then(|current| current.helper) {
291 helper.stop(&self.shared, true);
292 }
293 }
294
295 fn alive(&self) -> bool {
298 let state = self.state.borrow();
299 let alive = state
300 .as_ref()
301 .and_then(|state| state.request.try_with(|_| ()));
302 alive.is_some()
303 }
304
305 fn with_current<R>(&self, f: impl FnOnce(&mut Current) -> R) -> Option<R> {
307 let mut state = self.state.borrow_mut();
308 state.as_mut()?.current.as_mut().map(f)
309 }
310
311 fn publish(&self) {
313 let shown = self.state.borrow().as_ref().map(|state| {
314 let current = state.current.as_ref();
315 (
316 state.request,
317 current.map(|current| current.begin.shown.clone()),
318 )
319 });
320 if let Some((signal, shown)) = shown {
321 set_changed(signal, shown);
322 }
323 }
324
325 fn on_call(self: &Rc<Self>, call: Message) {
327 let header = call.header();
328 let sender = header.sender().map(|sender| sender.as_str());
329 let authority = self
330 .state
331 .borrow()
332 .as_ref()
333 .and_then(|state| state.authority.clone());
334 if sender.is_none() || sender != authority.as_deref() {
335 tracing::warn!(
336 ?sender,
337 "a program other than polkit called the agent; refused"
338 );
339 return self.reply(
340 &call,
341 Some((ACCESS_DENIED, "only polkit may call the agent")),
342 );
343 }
344 let path = header.path().map(|path| path.as_str());
345 let interface = header.interface().map(|interface| interface.as_str());
346 let member = header.member().map(|member| member.as_str());
347 match (path, interface, member) {
348 (Some(AGENT_PATH), Some(AGENT_INTERFACE), Some("BeginAuthentication")) => self.begin(call),
349 (Some(AGENT_PATH), Some(AGENT_INTERFACE), Some("CancelAuthentication")) => {
350 self.cancelled(call);
351 }
352 _ => self.reply(&call, Some((UNKNOWN_METHOD, "no such method"))),
353 }
354 }
355
356 fn begin(self: &Rc<Self>, call: Message) {
358 let begin = match begin_args(&call, self.next()) {
359 Ok(begin) => begin,
360 Err((error, why)) => {
361 tracing::warn!(why, "a polkit request the agent can't answer");
362 return self.reply(&call, Some((error, why)));
363 }
364 };
365 let refused = {
366 let mut state = self.state.borrow_mut();
367 let Some(state) = state.as_mut() else {
368 return;
369 };
370 let action = &begin.shown.action_id;
371 tracing::info!(action, "polkit asks to authenticate");
372 if state.current.is_none() {
373 state.current = Some(Current::new(begin));
374 None
375 } else if state.queue.len() < MAX_QUEUED {
376 state.queue.push_back(begin);
377 return;
378 } else {
379 Some(begin)
380 }
381 };
382 match refused {
383 Some(begin) => {
384 tracing::warn!("{MAX_QUEUED} polkit requests wait already; refused another");
385 self.reply(&begin.call, Some((FAILED, "too many requests wait")));
386 }
387 None => self.run_helper(),
388 }
389 }
390
391 fn cancelled(self: &Rc<Self>, call: Message) {
393 let Ok(cookie) = call.body().deserialize::<String>() else {
394 return self.reply(&call, Some((INVALID_ARGS, "not a cookie")));
395 };
396 self.reply(&call, None);
397 let why = "polkit cancelled it";
398 let current = self.with_current(|current| current.begin.cookie == cookie);
399 if current == Some(true) {
400 tracing::info!("polkit cancelled the request");
401 return self.finish(Some((CANCELLED, why)));
402 }
403 let queued = self.state.borrow_mut().as_mut().and_then(|state| {
404 let at = state
405 .queue
406 .iter()
407 .position(|begin| begin.cookie == cookie)?;
408 state.queue.remove(at)
409 });
410 if let Some(begin) = queued {
411 self.reply(&begin.call, Some((CANCELLED, why)));
412 }
413 }
414
415 fn pick(self: &Rc<Self>, name: &str) {
417 let picked = self.with_current(|current| {
418 let identities = &mut current.begin.shown.identities;
419 let at = identities.iter().position(|known| known == name)?;
420 identities[..=at].rotate_right(1);
421 current.answer = None;
422 Some(current.helper.take())
423 });
424 let Some(helper) = picked.flatten() else {
425 return tracing::warn!(name, "no such identity to pick for polkit");
426 };
427 if let Some(helper) = helper {
428 helper.stop(&self.shared, true);
429 }
430 self.run_helper();
431 }
432
433 fn run_helper(self: &Rc<Self>) {
435 let Some(event_loop) = self.shared.event_loop() else {
436 return;
437 };
438 let run = self.next();
439 let started = self.with_current(|current| {
440 let shown = &mut current.begin.shown;
441 shown.state = AuthState::Waiting;
442 shown.info.clear();
443 shown.errors.clear();
444 let config = &self.shared.config;
445 let user = &shown.identities[0];
446 let mut helper = Helper::start(&config.polkit_helper, &config.polkit_socket, user, run)?;
447 if let Err(error) = helper.send(current.begin.cookie.as_bytes()) {
448 helper.stop(&self.shared, true);
449 return Err(error);
450 }
451 let service = Rc::clone(self);
452 let ready = Box::new(move || service.on_output(run));
453 helper.token = helper
454 .stdout_fd()
455 .and_then(|fd| event_loop.insert_fd(fd, Interest::READ, ready));
456 if helper.token.is_none() {
457 helper.stop(&self.shared, true);
458 return Err(std::io::Error::other("its output couldn't be watched"));
459 }
460 current.helper = Some(helper);
461 Ok(())
462 });
463 match started {
464 Some(Ok(())) => self.publish(),
465 Some(Err(error)) => {
466 let program = self.shared.config.polkit_helper.display();
467 tracing::warn!(%error, %program, "couldn't start polkit's helper");
468 self.finish(Some((FAILED, "polkit's helper couldn't start")));
469 }
470 None => {}
471 }
472 }
473
474 fn on_output(self: &Rc<Self>, run: u64) {
476 let running = |service: &Service| {
477 let running =
478 service.with_current(|current| current.helper.as_ref().map(|helper| helper.run));
479 running.flatten() == Some(run)
480 };
481 if !running(self) {
482 return;
483 }
484 let read = self.with_current(|current| current.helper.as_mut()?.read());
485 let Some((lines, ended)) = read.flatten() else {
486 return;
487 };
488 for line in lines {
489 if !running(self) {
490 return;
491 }
492 self.on_line(helper::parse(&line));
493 }
494 if ended && running(self) {
495 tracing::warn!("polkit's helper ended without an answer");
496 self.failed();
497 }
498 }
499
500 fn on_line(self: &Rc<Self>, line: Line) {
501 let keep = |lines: &mut Vec<String>, text| {
502 if lines.len() == MAX_LINES {
503 lines.remove(0);
504 }
505 lines.push(text);
506 };
507 let verdict = line_verdict(&line);
508 let answer = self.with_current(|current| {
509 let shown = &mut current.begin.shown;
510 match line {
511 Line::Prompt { message, visible } => {
512 shown.state = AuthState::Asking { message, visible };
513 return current.answer.take();
514 }
515 Line::Info(text) => keep(&mut shown.info, text),
516 Line::Error(text) => keep(&mut shown.errors, text),
517 Line::Unknown => tracing::debug!("polkit's helper printed a line it doesn't know; skipped"),
518 Line::Success | Line::Failure => {}
519 }
520 None
521 });
522 match verdict {
523 Some(true) => {
524 tracing::info!("authenticated for polkit");
525 self.finish(None);
526 }
527 Some(false) => self.failed(),
528 None => match answer.flatten() {
529 Some(secret) => self.send(secret),
531 None => self.publish(),
532 },
533 }
534 }
535
536 fn failed(self: &Rc<Self>) {
539 let gave_up = self.with_current(|current| {
540 if let Some(helper) = current.helper.take() {
541 helper.stop(&self.shared, true);
542 }
543 current.answer = None;
544 current.failures += 1;
545 let shown = &mut current.begin.shown;
546 let message = shown.errors.last().cloned();
547 let message = message.unwrap_or_else(|| String::from("Authentication failed"));
548 shown.state = AuthState::Failed { message };
549 current.failures >= MAX_TRIES
550 });
551 match gave_up {
552 Some(true) => {
553 tracing::info!("polkit authentication failed {MAX_TRIES} times");
554 self.finish(Some((FAILED, "authentication failed")));
555 }
556 Some(false) => self.publish(),
557 None => {}
558 }
559 }
560
561 fn respond(self: &Rc<Self>, secret: Secret) {
563 if secret.0.contains(&b'\n') || secret.0.contains(&0) {
566 return tracing::warn!("a polkit answer with a line break or NUL was dropped");
567 }
568 let state = self.with_current(|current| current.begin.shown.state.clone());
569 match state {
570 Some(AuthState::Asking { .. }) => self.send(secret),
571 Some(AuthState::Failed { .. }) => {
572 self.with_current(|current| current.answer = Some(secret));
573 self.run_helper();
574 }
575 _ => tracing::debug!("no polkit question to answer; an answer was dropped"),
576 }
577 }
578
579 fn send(self: &Rc<Self>, secret: Secret) {
582 let sent = self.with_current(|current| {
583 let sent = current.helper.as_mut().map(|helper| helper.send(&secret.0));
584 if let Some(Ok(())) = sent {
585 current.begin.shown.state = AuthState::Busy;
586 }
587 sent
588 });
589 match sent.flatten() {
590 Some(Ok(())) => self.publish(),
591 Some(Err(error)) => {
592 tracing::warn!(%error, "couldn't answer polkit's helper");
593 self.failed();
594 }
595 None => {}
596 }
597 }
598
599 fn finish(self: &Rc<Self>, error: Option<(&'static str, &str)>) {
603 let current = self.state.borrow_mut().as_mut().and_then(|state| {
604 let current = state.current.take()?;
605 state.current = state.queue.pop_front().map(Current::new);
606 Some(current)
607 });
608 let Some(current) = current else {
609 return;
610 };
611 if let Some(helper) = current.helper {
612 helper.stop(&self.shared, error.is_some());
614 }
615 self.reply(¤t.begin.call, error);
616 match self.with_current(|_| ()) {
617 Some(()) => self.run_helper(),
618 None => self.publish(),
619 }
620 }
621
622 fn reply(&self, call: &Message, error: Option<(&str, &str)>) {
625 let header = call.header();
626 if header.primary().flags().contains(Flags::NO_REPLY_EXPECTED) {
627 return;
628 }
629 let reply = match error {
630 None => Message::method_return(&header).build(&()),
631 Some((name, why)) => Message::error(&header, name).build(&why),
632 };
633 let connection = self
634 .state
635 .borrow()
636 .as_ref()
637 .and_then(|state| state.connection.clone());
638 let Some(connection) = connection else {
639 return;
640 };
641 self.shared.spawn(async move {
642 let sent = match reply {
643 Ok(reply) => connection.send(&reply).await,
644 Err(error) => Err(error),
645 };
646 if let Err(error) = sent {
647 tracing::debug!(%error, "the polkit agent's answer wasn't sent");
648 }
649 });
650 }
651}
652
653fn line_verdict(line: &Line) -> Option<bool> {
656 match line {
657 Line::Success => Some(true),
658 Line::Failure => Some(false),
659 _ => None,
660 }
661}
662
663type BeginArgs<'a> = (
666 &'a str,
667 &'a str,
668 &'a str,
669 HashMap<&'a str, &'a str>,
670 &'a str,
671 Vec<(&'a str, HashMap<&'a str, OwnedVariant>)>,
672);
673
674fn begin_args(call: &Message, id: u64) -> Result<Begin, (&'static str, &'static str)> {
676 let body = call.body();
677 let Ok((action_id, message, icon_name, _, cookie, identities)) =
678 body.deserialize::<BeginArgs<'_>>()
679 else {
680 return Err((INVALID_ARGS, "not a polkit request"));
681 };
682 if cookie.is_empty() || cookie.len() > MAX_TEXT || cookie.contains('\n') {
684 return Err((FAILED, "a bad cookie"));
685 }
686 let passwd = std::fs::read_to_string("/etc/passwd").unwrap_or_default();
687 let mut users: Vec<(u32, String)> = identities
688 .iter()
689 .take(MAX_IDENTITIES)
690 .filter(|(kind, _)| *kind == "unix-user")
691 .filter_map(|(_, details)| u32::try_from(details.get("uid")?).ok())
692 .filter_map(|uid| Some((uid, user_name(&passwd, uid)?)))
693 .collect();
694 let own = rustix::process::getuid().as_raw();
697 if let Some(at) = users.iter().position(|(uid, _)| *uid == own) {
698 users[..=at].rotate_right(1);
699 }
700 let names: Vec<String> = users.into_iter().map(|(_, name)| name).collect();
701 if names.is_empty() {
702 return Err((FAILED, "no user the agent knows may answer"));
703 }
704 Ok(Begin {
705 call: call.clone(),
706 cookie: cookie.to_owned(),
707 shown: AuthRequest {
708 id,
709 action_id: cut(action_id, MAX_TEXT),
710 message: cut(message, MAX_TEXT),
711 icon_name: cut(icon_name, MAX_TEXT),
712 identities: names,
713 state: AuthState::Waiting,
714 info: Vec::new(),
715 errors: Vec::new(),
716 },
717 })
718}
719
720fn cut(text: &str, max: usize) -> String {
722 text[..text.floor_char_boundary(max)].to_owned()
723}
724
725fn user_name(passwd: &str, uid: u32) -> Option<String> {
730 let listed = passwd.lines().find_map(|line| {
731 let mut fields = line.split(':');
732 let name = fields.next()?;
733 let uid_field = fields.nth(1)?;
734 (uid_field.parse() == Ok(uid)).then(|| name.to_owned())
735 });
736 let own = || {
737 let ours = rustix::process::getuid().as_raw() == uid;
738 ours.then(|| std::env::var("USER").ok()).flatten()
739 };
740 listed.or_else(own).filter(|name| !name.is_empty())
741}
742
743fn subject(session: Option<String>) -> (&'static str, HashMap<&'static str, Variant<'static>>) {
746 match session {
747 Some(id) => (
748 "unix-session",
749 HashMap::from([("session-id", Variant::from(id))]),
750 ),
751 None => (
752 "unix-process",
753 HashMap::from([
754 ("pid", Variant::from(std::process::id())),
755 ("start-time", Variant::from(start_time().unwrap_or(0))),
756 ]),
757 ),
758 }
759}
760
761fn start_time() -> Option<u64> {
764 let stat = std::fs::read_to_string("/proc/self/stat").ok()?;
765 stat
767 .rsplit_once(')')?
768 .1
769 .split_whitespace()
770 .nth(19)?
771 .parse()
772 .ok()
773}
774
775async fn run(service: Rc<Service>, address: zbus::Address) {
779 let connection = match zbus::connection::Builder::address(address).build().await {
780 Ok(connection) => connection,
781 Err(error) => {
782 return tracing::info!(%error, "can't reach the system bus, so no polkit requests arrive");
783 }
784 };
785 let mut messages = MessageStream::from(&connection);
787 if let Err(error) = bus_call(&connection, "AddMatch", &owner_rule(AUTHORITY)).await {
788 return tracing::warn!(%error, "can't follow polkit on the system bus, so no requests arrive");
789 }
790 if let Some(state) = service.state.borrow_mut().as_mut() {
791 state.connection = Some(connection.clone());
792 }
793 register(&service, &connection).await;
794 loop {
795 let next = poll_fn(|cx| Pin::new(&mut messages).poll_next(cx)).await;
796 if !service.alive() {
797 return;
798 }
799 match next {
800 Some(Ok(message)) if message.header().message_type() == Kind::MethodCall => {
801 service.on_call(message);
802 }
803 Some(Ok(message)) => {
804 if let Some((name, owner)) = owner_changed(&message)
806 && name == AUTHORITY
807 {
808 service.drop_requests();
810 service.publish();
811 if !owner.is_empty() {
812 tracing::info!("polkit started again; registering the agent with it");
813 register(&service, &connection).await;
814 }
815 }
816 }
817 Some(Err(error)) => tracing::warn!(%error, "a bad message on the system bus"),
818 None => break,
819 }
820 }
821 tracing::warn!("the system bus closed, so no more polkit requests arrive");
822 service.forget();
823 service.publish();
824}
825
826async fn register(service: &Rc<Service>, connection: &Connection) {
829 let session = std::env::var("XDG_SESSION_ID")
830 .ok()
831 .filter(|id| !id.is_empty());
832 let locale = service.shared.config.desktop_entries.locale.as_str();
834 let body = (subject(session), locale, AGENT_PATH);
835 let call = connection.call_method(
836 Some(AUTHORITY),
837 AUTHORITY_PATH,
838 Some(AUTHORITY_INTERFACE),
839 "RegisterAuthenticationAgent",
840 &body,
841 );
842 let authority = match service.shared.timeout(CALL_TIMEOUT, call).await {
843 Ok(reply) => reply.header().sender().map(|sender| sender.to_string()),
844 Err(error) => return tracing::warn!(%error, "polkit refused the agent, so no requests arrive"),
845 };
846 tracing::info!("registered as the polkit agent");
847 if let Some(state) = service.state.borrow_mut().as_mut() {
848 state.authority = authority;
849 }
850}